fly-ftui 0.1.0__tar.gz
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.
- fly_ftui-0.1.0/LICENSE +21 -0
- fly_ftui-0.1.0/PKG-INFO +55 -0
- fly_ftui-0.1.0/README.md +38 -0
- fly_ftui-0.1.0/build/lib/ftui/app.py +252 -0
- fly_ftui-0.1.0/build/lib/ftui/fly_client.py +107 -0
- fly_ftui-0.1.0/build/lib/ftui/main.py +17 -0
- fly_ftui-0.1.0/main.py +6 -0
- fly_ftui-0.1.0/pyproject.toml +31 -0
- fly_ftui-0.1.0/src/ftui/app.py +302 -0
- fly_ftui-0.1.0/src/ftui/client.py +165 -0
- fly_ftui-0.1.0/src/ftui/fly_client.py +133 -0
- fly_ftui-0.1.0/src/ftui/main.py +18 -0
- fly_ftui-0.1.0/src/ftui/styles.tcss +58 -0
- fly_ftui-0.1.0/src/ftui.egg-info/PKG-INFO +48 -0
- fly_ftui-0.1.0/src/ftui.egg-info/SOURCES.txt +11 -0
- fly_ftui-0.1.0/src/ftui.egg-info/dependency_links.txt +1 -0
- fly_ftui-0.1.0/src/ftui.egg-info/entry_points.txt +2 -0
- fly_ftui-0.1.0/src/ftui.egg-info/requires.txt +3 -0
- fly_ftui-0.1.0/src/ftui.egg-info/top_level.txt +1 -0
- fly_ftui-0.1.0/uv.lock +157 -0
fly_ftui-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fly.io Community
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fly_ftui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fly-ftui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal UI for managing Fly.io machines
|
|
5
|
+
Author: Fly.io Community
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: cli,fly,flyio,textual,tui
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: click>=8.3.1
|
|
14
|
+
Requires-Dist: rich>=14.3.3
|
|
15
|
+
Requires-Dist: textual>=8.1.1
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ftui - Fly.io Terminal UI
|
|
19
|
+
|
|
20
|
+
A Python-based TUI for managing your Fly.io machines.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
- **List Machines:** View all machines in your current Fly app context.
|
|
24
|
+
- **Real-time Logs:** Stream logs for specific machines.
|
|
25
|
+
- **Scaling:** Quickly scale machine count and VM size.
|
|
26
|
+
- **SSH Access:** Interactive shell access to your machines.
|
|
27
|
+
- **Machine Control:** Start and stop machines directly from the UI.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
Run in a directory with a `fly.toml` file:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ftui
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For development/testing without a live account:
|
|
44
|
+
```bash
|
|
45
|
+
ftui --mock
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Keybindings
|
|
49
|
+
- `r`: Refresh machine list
|
|
50
|
+
- `l`: View logs for selected machine
|
|
51
|
+
- `s`: Scale app (count/VM size)
|
|
52
|
+
- `h`: SSH into selected machine
|
|
53
|
+
- `Ctrl+s`: Start selected machine
|
|
54
|
+
- `Ctrl+x`: Stop selected machine
|
|
55
|
+
- `q`: Quit / Back
|
fly_ftui-0.1.0/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ftui - Fly.io Terminal UI
|
|
2
|
+
|
|
3
|
+
A Python-based TUI for managing your Fly.io machines.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **List Machines:** View all machines in your current Fly app context.
|
|
7
|
+
- **Real-time Logs:** Stream logs for specific machines.
|
|
8
|
+
- **Scaling:** Quickly scale machine count and VM size.
|
|
9
|
+
- **SSH Access:** Interactive shell access to your machines.
|
|
10
|
+
- **Machine Control:** Start and stop machines directly from the UI.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv tool install .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
Run in a directory with a `fly.toml` file:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ftui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For development/testing without a live account:
|
|
27
|
+
```bash
|
|
28
|
+
ftui --mock
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Keybindings
|
|
32
|
+
- `r`: Refresh machine list
|
|
33
|
+
- `l`: View logs for selected machine
|
|
34
|
+
- `s`: Scale app (count/VM size)
|
|
35
|
+
- `h`: SSH into selected machine
|
|
36
|
+
- `Ctrl+s`: Start selected machine
|
|
37
|
+
- `Ctrl+x`: Stop selected machine
|
|
38
|
+
- `q`: Quit / Back
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Dict, Any, List
|
|
5
|
+
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.widgets import Header, Footer, DataTable, Static, RichLog, Input, Label
|
|
8
|
+
from textual.screen import Screen, ModalScreen
|
|
9
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
|
|
12
|
+
from ftui.fly_client import FlyClient
|
|
13
|
+
|
|
14
|
+
class MachineListScreen(Screen):
|
|
15
|
+
"""Screen for listing and managing Fly machines."""
|
|
16
|
+
|
|
17
|
+
BINDINGS = [
|
|
18
|
+
Binding("r", "refresh", "Refresh", show=True),
|
|
19
|
+
Binding("l", "view_logs", "Logs", show=True),
|
|
20
|
+
Binding("s", "scale", "Scale", show=True),
|
|
21
|
+
Binding("h", "ssh", "SSH", show=True),
|
|
22
|
+
Binding("ctrl+s", "start_machine", "Start", show=True),
|
|
23
|
+
Binding("ctrl+x", "stop_machine", "Stop", show=True),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def __init__(self, fly_client: FlyClient):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.fly = fly_client
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
yield Header()
|
|
32
|
+
yield DataTable(id="machine-table", cursor_type="row")
|
|
33
|
+
yield Footer()
|
|
34
|
+
|
|
35
|
+
async def on_mount(self) -> None:
|
|
36
|
+
table = self.query_one(DataTable)
|
|
37
|
+
table.add_columns("ID", "Name", "State", "Region", "Image", "Created")
|
|
38
|
+
await self.action_refresh()
|
|
39
|
+
# Set a timer for periodic refresh
|
|
40
|
+
self.set_interval(10, self.action_refresh)
|
|
41
|
+
|
|
42
|
+
async def action_refresh(self) -> None:
|
|
43
|
+
"""Fetch machine list and update table."""
|
|
44
|
+
table = self.query_one(DataTable)
|
|
45
|
+
try:
|
|
46
|
+
machines = await self.fly.list_machines()
|
|
47
|
+
table.clear()
|
|
48
|
+
for m in machines:
|
|
49
|
+
table.add_row(
|
|
50
|
+
m.get("id", ""),
|
|
51
|
+
m.get("name", ""),
|
|
52
|
+
m.get("state", ""),
|
|
53
|
+
m.get("region", ""),
|
|
54
|
+
m.get("image", ""),
|
|
55
|
+
m.get("created_at", "")[:19] # Truncate timestamp
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.notify(f"Error fetching machines: {e}", severity="error")
|
|
59
|
+
|
|
60
|
+
def get_selected_machine(self) -> str:
|
|
61
|
+
"""Get the ID of the selected machine in the table."""
|
|
62
|
+
table = self.query_one(DataTable)
|
|
63
|
+
if table.cursor_row is not None:
|
|
64
|
+
row = table.get_row_at(table.cursor_row)
|
|
65
|
+
return row[0]
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
async def action_view_logs(self) -> None:
|
|
69
|
+
machine_id = self.get_selected_machine()
|
|
70
|
+
if machine_id:
|
|
71
|
+
self.app.push_screen(LogScreen(self.fly, machine_id))
|
|
72
|
+
else:
|
|
73
|
+
self.notify("No machine selected")
|
|
74
|
+
|
|
75
|
+
async def action_scale(self) -> None:
|
|
76
|
+
self.app.push_screen(ScaleDialog(self.fly))
|
|
77
|
+
|
|
78
|
+
async def action_ssh(self) -> None:
|
|
79
|
+
machine_id = self.get_selected_machine()
|
|
80
|
+
if not machine_id:
|
|
81
|
+
self.notify("No machine selected")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# We need to suspend the TUI to run an interactive shell
|
|
85
|
+
self.app.suspend_stdio()
|
|
86
|
+
try:
|
|
87
|
+
print(f"Connecting to {machine_id}...")
|
|
88
|
+
# Use os.system for simple handover to the interactive fly ssh command
|
|
89
|
+
cmd = f"fly ssh console -s -m {machine_id}"
|
|
90
|
+
if self.fly.mock:
|
|
91
|
+
print(f"MOCK: {cmd}")
|
|
92
|
+
input("Press Enter to return to FTUI...")
|
|
93
|
+
else:
|
|
94
|
+
os.system(cmd)
|
|
95
|
+
finally:
|
|
96
|
+
self.app.resume_stdio()
|
|
97
|
+
|
|
98
|
+
async def action_start_machine(self) -> None:
|
|
99
|
+
machine_id = self.get_selected_machine()
|
|
100
|
+
if machine_id:
|
|
101
|
+
try:
|
|
102
|
+
await self.fly.start_machine(machine_id)
|
|
103
|
+
self.notify(f"Machine {machine_id} started")
|
|
104
|
+
await self.action_refresh()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
self.notify(f"Failed to start machine: {e}", severity="error")
|
|
107
|
+
|
|
108
|
+
async def action_stop_machine(self) -> None:
|
|
109
|
+
machine_id = self.get_selected_machine()
|
|
110
|
+
if machine_id:
|
|
111
|
+
try:
|
|
112
|
+
await self.fly.stop_machine(machine_id)
|
|
113
|
+
self.notify(f"Machine {machine_id} stopped")
|
|
114
|
+
await self.action_refresh()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.notify(f"Failed to stop machine: {e}", severity="error")
|
|
117
|
+
|
|
118
|
+
class LogScreen(Screen):
|
|
119
|
+
"""Screen for viewing machine logs."""
|
|
120
|
+
|
|
121
|
+
BINDINGS = [
|
|
122
|
+
Binding("q", "back", "Back", show=True),
|
|
123
|
+
Binding("c", "clear", "Clear", show=True),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
def __init__(self, fly_client: FlyClient, machine_id: str):
|
|
127
|
+
super().__init__()
|
|
128
|
+
self.fly = fly_client
|
|
129
|
+
self.machine_id = machine_id
|
|
130
|
+
self.process = None
|
|
131
|
+
|
|
132
|
+
def compose(self) -> ComposeResult:
|
|
133
|
+
yield Header()
|
|
134
|
+
yield RichLog(id="log-viewer", highlight=True, markup=True)
|
|
135
|
+
yield Footer()
|
|
136
|
+
|
|
137
|
+
async def on_mount(self) -> None:
|
|
138
|
+
log_viewer = self.query_one(RichLog)
|
|
139
|
+
log_viewer.write(f"Tail logs for machine [bold cyan]{self.machine_id}[/bold cyan]...")
|
|
140
|
+
|
|
141
|
+
# Start log tailing
|
|
142
|
+
asyncio.create_task(self.stream_logs())
|
|
143
|
+
|
|
144
|
+
async def stream_logs(self):
|
|
145
|
+
log_viewer = self.query_one(RichLog)
|
|
146
|
+
|
|
147
|
+
if self.fly.mock:
|
|
148
|
+
while True:
|
|
149
|
+
log_viewer.write(f"[{self.machine_id}] Simulated log message {random.randint(100,999)}")
|
|
150
|
+
await asyncio.sleep(1)
|
|
151
|
+
if self.is_current_screen is False:
|
|
152
|
+
break
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
args = ["logs", "--instance", self.machine_id]
|
|
156
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
157
|
+
"fly", *args,
|
|
158
|
+
stdout=asyncio.subprocess.PIPE,
|
|
159
|
+
stderr=asyncio.subprocess.STDOUT
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
while True:
|
|
163
|
+
line = await self.process.stdout.readline()
|
|
164
|
+
if not line:
|
|
165
|
+
break
|
|
166
|
+
log_viewer.write(line.decode().strip())
|
|
167
|
+
|
|
168
|
+
def action_back(self) -> None:
|
|
169
|
+
if self.process:
|
|
170
|
+
self.process.terminate()
|
|
171
|
+
self.app.pop_screen()
|
|
172
|
+
|
|
173
|
+
def action_clear(self) -> None:
|
|
174
|
+
self.query_one(RichLog).clear()
|
|
175
|
+
|
|
176
|
+
class ScaleDialog(ModalScreen):
|
|
177
|
+
"""Modal for scaling applications."""
|
|
178
|
+
|
|
179
|
+
def compose(self) -> ComposeResult:
|
|
180
|
+
with Container(id="dialog"):
|
|
181
|
+
yield Label("Scale Application", id="title")
|
|
182
|
+
yield Label("Machine Count:")
|
|
183
|
+
yield Input(placeholder="Enter count (e.g. 3)", id="count-input")
|
|
184
|
+
yield Label("VM Size:")
|
|
185
|
+
yield Input(placeholder="Enter size (e.g. shared-cpu-1x)", id="vm-input")
|
|
186
|
+
with Horizontal():
|
|
187
|
+
yield Static(id="spacer")
|
|
188
|
+
yield Vertical(
|
|
189
|
+
Horizontal(
|
|
190
|
+
Static(" "),
|
|
191
|
+
Static("[b]Enter[/b] to Apply, [b]Esc[/b] to Cancel", id="help-text"),
|
|
192
|
+
classes="dialog-buttons"
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def on_key(self, event) -> None:
|
|
197
|
+
if event.key == "escape":
|
|
198
|
+
self.app.pop_screen()
|
|
199
|
+
|
|
200
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
201
|
+
count_val = self.query_one("#count-input", Input).value
|
|
202
|
+
vm_val = self.query_one("#vm-input", Input).value
|
|
203
|
+
|
|
204
|
+
fly = FlyClient() # Re-using FlyClient
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
if count_val:
|
|
208
|
+
await fly.scale_count(int(count_val))
|
|
209
|
+
self.notify(f"Scaling count to {count_val}...")
|
|
210
|
+
|
|
211
|
+
if vm_val:
|
|
212
|
+
await fly.scale_vm(vm_val)
|
|
213
|
+
self.notify(f"Scaling VM size to {vm_val}...")
|
|
214
|
+
|
|
215
|
+
self.app.pop_screen()
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self.notify(f"Scale error: {e}", severity="error")
|
|
218
|
+
|
|
219
|
+
class FTUI(App):
|
|
220
|
+
"""Main Fly.io TUI application."""
|
|
221
|
+
|
|
222
|
+
CSS = """
|
|
223
|
+
#dialog {
|
|
224
|
+
width: 40;
|
|
225
|
+
height: auto;
|
|
226
|
+
border: thick $primary;
|
|
227
|
+
background: $surface;
|
|
228
|
+
padding: 1 2;
|
|
229
|
+
align: center middle;
|
|
230
|
+
}
|
|
231
|
+
#title {
|
|
232
|
+
text-align: center;
|
|
233
|
+
width: 100%;
|
|
234
|
+
margin-bottom: 1;
|
|
235
|
+
background: $primary;
|
|
236
|
+
color: $on-primary;
|
|
237
|
+
}
|
|
238
|
+
.dialog-buttons {
|
|
239
|
+
margin-top: 1;
|
|
240
|
+
}
|
|
241
|
+
#spacer {
|
|
242
|
+
width: 1fr;
|
|
243
|
+
}
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def on_mount(self) -> None:
|
|
247
|
+
self.fly = FlyClient()
|
|
248
|
+
self.push_screen(MachineListScreen(self.fly))
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
app = FTUI()
|
|
252
|
+
app.run()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
class FlyClient:
|
|
8
|
+
"""Wrapper for flyctl commands."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, mock: bool = False):
|
|
11
|
+
self.mock = mock or os.getenv("FTUI_MOCK") == "1"
|
|
12
|
+
|
|
13
|
+
async def _run(self, args: List[str]) -> str:
|
|
14
|
+
"""Run a flyctl command and return its output."""
|
|
15
|
+
if self.mock:
|
|
16
|
+
return await self._mock_run(args)
|
|
17
|
+
|
|
18
|
+
proc = await asyncio.create_subprocess_exec(
|
|
19
|
+
"fly", *args,
|
|
20
|
+
stdout=asyncio.subprocess.PIPE,
|
|
21
|
+
stderr=asyncio.subprocess.PIPE
|
|
22
|
+
)
|
|
23
|
+
stdout, stderr = await proc.communicate()
|
|
24
|
+
|
|
25
|
+
if proc.returncode != 0:
|
|
26
|
+
raise Exception(f"flyctl error: {stderr.decode()}")
|
|
27
|
+
|
|
28
|
+
return stdout.decode()
|
|
29
|
+
|
|
30
|
+
async def _mock_run(self, args: List[str]) -> str:
|
|
31
|
+
"""Simulate flyctl command outputs."""
|
|
32
|
+
await asyncio.sleep(0.1) # Simulate network latency
|
|
33
|
+
|
|
34
|
+
cmd = " ".join(args)
|
|
35
|
+
if "machines list" in cmd and "--json" in cmd:
|
|
36
|
+
return json.dumps([
|
|
37
|
+
{
|
|
38
|
+
"id": "148ed106c62289",
|
|
39
|
+
"name": "gentle-sun-42",
|
|
40
|
+
"state": "started",
|
|
41
|
+
"region": "ams",
|
|
42
|
+
"image": "flyio/hellofly:latest",
|
|
43
|
+
"created_at": "2023-10-01T12:00:00Z"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "7811d615b04489",
|
|
47
|
+
"name": "bitter-cloud-12",
|
|
48
|
+
"state": "stopped",
|
|
49
|
+
"region": "ams",
|
|
50
|
+
"image": "flyio/hellofly:latest",
|
|
51
|
+
"created_at": "2023-10-02T10:00:00Z"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "9080e21fb54287",
|
|
55
|
+
"name": "patient-field-77",
|
|
56
|
+
"state": "started",
|
|
57
|
+
"region": "lhr",
|
|
58
|
+
"image": "flyio/hellofly:latest",
|
|
59
|
+
"created_at": "2023-10-03T08:30:00Z"
|
|
60
|
+
}
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
if "scale show" in cmd and "--json" in cmd:
|
|
64
|
+
return json.dumps({
|
|
65
|
+
"ProcessGroup": "app",
|
|
66
|
+
"Count": 3,
|
|
67
|
+
"CPUs": 1,
|
|
68
|
+
"Memory": 256,
|
|
69
|
+
"VMSize": "shared-cpu-1x"
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
async def list_machines(self) -> List[Dict[str, Any]]:
|
|
75
|
+
"""List machines for the current app."""
|
|
76
|
+
output = await self._run(["machines", "list", "--json"])
|
|
77
|
+
if not output.strip():
|
|
78
|
+
return []
|
|
79
|
+
return json.loads(output)
|
|
80
|
+
|
|
81
|
+
async def get_scale(self) -> Dict[str, Any]:
|
|
82
|
+
"""Get scaling information for the current app."""
|
|
83
|
+
# Using machine list to infer count and VM size as 'scale show' can be complex to parse
|
|
84
|
+
# but let's try 'scale show --json' if supported or mock it.
|
|
85
|
+
try:
|
|
86
|
+
output = await self._run(["scale", "show", "--json"])
|
|
87
|
+
return json.loads(output)
|
|
88
|
+
except:
|
|
89
|
+
# Fallback for older flyctl versions or if json output is not available
|
|
90
|
+
machines = await self.list_machines()
|
|
91
|
+
return {"Count": len(machines), "VMSize": "unknown"}
|
|
92
|
+
|
|
93
|
+
async def scale_count(self, count: int):
|
|
94
|
+
"""Scale the machine count."""
|
|
95
|
+
await self._run(["scale", "count", str(count), "--yes"])
|
|
96
|
+
|
|
97
|
+
async def scale_vm(self, size: str):
|
|
98
|
+
"""Scale the VM size."""
|
|
99
|
+
await self._run(["scale", "vm", size, "--yes"])
|
|
100
|
+
|
|
101
|
+
async def stop_machine(self, machine_id: str):
|
|
102
|
+
"""Stop a specific machine."""
|
|
103
|
+
await self._run(["machine", "stop", machine_id])
|
|
104
|
+
|
|
105
|
+
async def start_machine(self, machine_id: str):
|
|
106
|
+
"""Start a specific machine."""
|
|
107
|
+
await self._run(["machine", "start", machine_id])
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
from ftui.app import FTUI
|
|
4
|
+
from ftui.fly_client import FlyClient
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.option("--mock", is_flag=True, help="Use mock flyctl for development")
|
|
8
|
+
def main(mock: bool):
|
|
9
|
+
"""Fly.io Terminal UI (ftui)"""
|
|
10
|
+
if mock:
|
|
11
|
+
os.environ["FTUI_MOCK"] = "1"
|
|
12
|
+
|
|
13
|
+
app = FTUI()
|
|
14
|
+
app.run()
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
fly_ftui-0.1.0/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fly-ftui"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A terminal UI for managing Fly.io machines"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Fly.io Community" }
|
|
9
|
+
]
|
|
10
|
+
keywords = ["fly", "flyio", "tui", "textual", "cli"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"textual>=8.1.1",
|
|
19
|
+
"rich>=14.3.3",
|
|
20
|
+
"click>=8.3.1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
ftui = "ftui.main:main"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/ftui"]
|