-
Notifications
You must be signed in to change notification settings - Fork 295
Expand file tree
/
Copy pathsetup.py
More file actions
350 lines (289 loc) · 14 KB
/
Copy pathsetup.py
File metadata and controls
350 lines (289 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
import sys
import subprocess
import shutil
import platform
import os
import textwrap
import urllib.request
import tempfile
import json
from pathlib import Path
from typing import List, Dict, Optional
# Install third-party packages if missing
for pkg in ["rich", "questionary"]:
try:
__import__(pkg)
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.align import Align
from rich import box
from rich.markdown import Markdown
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
import questionary
console = Console()
STATE_FILE = ".vibe.json"
FIREBASE_CLI = "firebase.cmd" if platform.system() == "Windows" else "firebase"
NPM_CLI = "npm.cmd" if platform.system() == "Windows" else "npm"
def clear_screen():
if platform.system() == "Windows":
subprocess.run(["cls"], shell=True)
else:
subprocess.run(["clear"], shell=True)
# ------------------ Pipeline State Manager ------------------
class SetupState:
def __init__(self):
self.state = {}
self.load()
def load(self):
if os.path.exists(STATE_FILE):
with open(STATE_FILE, "r") as f:
self.state = json.load(f)
def save(self):
with open(STATE_FILE, "w") as f:
json.dump(self.state, f, indent=2)
def update(self, key: str, value):
self.state[key] = value
self.save()
def get(self, key: str, default=None):
return self.state.get(key, default)
def show_summary(self):
console.print("\n[bold cyan]Setup Summary[/bold cyan]")
self.print_final_progress_table()
def print_final_progress_table(self):
table = Table(title="ViBe Setup Progress", box=box.ROUNDED)
table.add_column("#", justify="center")
table.add_column("Step", justify="left")
table.add_column("Description", justify="left")
table.add_column("Status", justify="center")
steps = self.get("steps", [])
if not steps:
console.print("[red]No steps found in state. Cannot show summary.[/red]")
return
for index, step in enumerate(steps, 1):
if self.get(step["name"]): #Access name as a dictionary key
status = "✅"
else:
status = "❌"
table.add_row(str(index), step["name"], step["description"], status) #Access name and description as dictionary keys
console.print(table)
# ------------------ Base Step Class ------------------
class PipelineStep:
def __init__(self, name: str, description: str, instructions: Optional[str] = None):
self.name = name
self.description = description
self.instructions = instructions
def should_run(self, state: SetupState) -> bool:
return not state.get(self.name)
def run(self, state: SetupState):
raise NotImplementedError("Each step must implement a run method")
def display_instructions(self):
if self.instructions:
console.print(Panel(self.instructions, title=f"[bold cyan]{self.name} - Instructions[/bold cyan]"))
# ------------------ Step Implementations ------------------
class WelcomeStep(PipelineStep):
def __init__(self):
super().__init__("Welcome", "Select environment")
def run(self, state):
title = Text("🚀 ViBe Setup Wizard 🚀", style="bold white on blue", justify="center")
console.print(Align.center(title))
panel = Panel("[green]Welcome to the ViBe backend setup process![/green]", title="[bold cyan]Welcome[/bold cyan]", border_style="green", box=box.ROUNDED)
console.print("\n")
console.print(panel)
console.print("\n")
environment = questionary.select("Choose environment:", choices=["Development", "Production"]).ask()
state.update("environment", environment)
if environment == "Development":
pass
elif environment == "Production":
console.print("[red]Production setup is not ready yet.[/red]")
sys.exit(1)
state.update(self.name, True)
return environment
class ToolchainCheckStep(PipelineStep):
def __init__(self):
super().__init__("ToolChain Check", "Verify Node.js, npm, pnpm, and firebase-tools are installed")
def run(self, state):
def check_command_exists(command):
return shutil.which(command) is not None
if not check_command_exists("node"):
console.print("[yellow]⚠ Node.js is not installed. Installing using fnm...[/yellow]")
try:
if platform.system() == "Windows":
subprocess.run(["winget", "install", "Schniz.fnm"], check=True)
else:
subprocess.run("curl -o- https://fnm.vercel.app/install | bash", check=True, shell=True)
subprocess.run(["fnm", "install", "22"], check=True)
except subprocess.CalledProcessError as e:
console.print(f"[red]❌ Failed to install Node.js: {e}[/red]")
sys.exit(1)
console.print("[green]✅ Node.js installed successfully.[/green]")
if not check_command_exists("npm"):
console.print("[red]❌ npm is not installed.")
sys.exit(1)
if not check_command_exists("pnpm"):
console.print("[yellow]⚠ Installing pnpm...[/yellow]")
subprocess.run([NPM_CLI, "install", "-g", "pnpm"], check=True, shell=(platform.system() == "Windows"))
if not check_command_exists("firebase"):
console.print("[yellow]⚠ Installing firebase-tools...[/yellow]")
subprocess.run(["pnpm", "install", "-g", "firebase-tools"], check=True, shell=(platform.system() == "Windows"))
console.print(":white_check_mark: [green]Toolchain verified.[/green]")
# run pnpm install command in the current directory
console.print("[yellow]⚠ Installing pnpm dependencies...[/yellow]")
subprocess.run(["pnpm", "install"], check=True, shell=(platform.system() == "Windows"))
console.print("[green]✅ pnpm dependencies installed successfully.[/green]")
state.update(self.name, True)
class FirebaseLoginStep(PipelineStep):
def __init__(self):
super().__init__("Firebase Login", "Ensure Firebase CLI is logged in")
def run(self, state):
result = subprocess.run([FIREBASE_CLI, "login:list"], capture_output=True, text=True, shell=(platform.system() == "Windows"))
if "No authorized accounts" in result.stdout:
subprocess.run([FIREBASE_CLI, "login"], check=True, shell=(platform.system() == "Windows"))
state.update(self.name, True)
class FirebaseEmulatorsStep(PipelineStep):
def __init__(self, backend_dir):
super().__init__("Emulators", "Initialize Firebase emulators",
instructions="Please choose ONLY the following emulators when prompted:\n\n✔ Authentication Emulator\n✔ Functions Emulator\n✔ Emulator UI [optional but recommended]")
self.backend_dir = backend_dir
def run(self, state):
subprocess.run([FIREBASE_CLI, "init", "emulators"], cwd=self.backend_dir, check=True, shell=(platform.system() == "Windows"))
state.update(self.name, True)
class EnvFileStep(PipelineStep):
def __init__(self, backend_dir):
super().__init__("Env Variables", "Create .env file and set MongoDB URI",
instructions="""
[bold]MongoDB URI Instructions:[/bold]
1. Go to [link=https://www.mongodb.com/atlas/database]MongoDB Atlas[/link] and sign up or log in.
2. Create a new project and cluster.
3. Navigate to the 'Database Access' section in your project.
4. Create a new database user with appropriate permissions.
5. Go to the 'Clusters' section and click 'Connect' on your cluster.
6. Select 'Connect your application'.
7. Copy the connection string.
8. [bold red]Replace '<password>' in the copied string with the actual password[/bold red] you created for the database user.
9. Paste the modified connection string below.
""")
self.backend_dir = backend_dir
def run(self, state):
env_path = os.path.join(self.backend_dir, ".env")
if not os.path.exists(env_path):
uri = questionary.text("Paste your MongoDB URI:").ask()
with open(env_path, "w") as f:
f.write(f"DB_URL=\"{uri}\"\n")
state.update(self.name, True)
class PackageInstallStep(PipelineStep):
def __init__(self, backend_dir):
super().__init__("Backend Packages", "Install backend dependencies")
self.backend_dir = backend_dir
def run(self, state):
with console.status("Installing backend dependencies..."):
subprocess.run(["pnpm", "install"], cwd=self.backend_dir, check=True, shell=(platform.system() == "Windows"))
state.update(self.name, True)
class MongoDBBinaryStep(PipelineStep):
def __init__(self, backend_dir):
super().__init__("MongoDB Test Binaries", "Ensure MongoDB binaries for in-memory server are downloaded")
self.backend_dir = backend_dir
def run(self, state):
console.print("[cyan]Ensuring MongoDB binaries are downloaded for mongodb-memory-server...[/cyan]")
script = textwrap.dedent("""
import { MongoMemoryServer } from 'mongodb-memory-server';
(async () => {
const mongod = await MongoMemoryServer.create();
await mongod.getUri();
await mongod.stop();
})();
""")
try:
subprocess.run(["pnpx", "ts-node", "-e", script], check=True, cwd=self.backend_dir, shell=(platform.system() == "Windows"))
state.update(self.name, True)
except subprocess.CalledProcessError as e:
console.print(f"[red]❌ Failed to download MongoDB binaries: {e}[/red]")
sys.exit(1)
class TestStep(PipelineStep):
def __init__(self, backend_dir):
super().__init__("Backend Tests", "Run backend tests")
self.backend_dir = backend_dir
def run(self, state):
with console.status("Running backend tests..."):
result = subprocess.run(["pnpm", "run", "test:ci"], cwd=self.backend_dir, shell=(platform.system() == "Windows"))
if result.returncode == 0:
console.print("[green]✅ All tests passed! Backend setup complete.")
state.update(self.name, True)
else:
console.print("[red]❌ Tests failed. Please fix and re-run the setup.")
sys.exit(1)
class FrontendPackageInstallStep(PipelineStep):
def __init__(self, frontend_dir):
super().__init__("Frontend Packages", "Install frontend dependencies")
self.frontend_dir = frontend_dir
def run(self, state):
subprocess.run(["pnpm", "install"], cwd=self.frontend_dir, check=True, shell=(platform.system() == "Windows"))
state.update(self.name, True)
# ------------------ Pipeline Manager ------------------
class SetupPipeline:
def __init__(self, steps: List[PipelineStep], state: SetupState):
self.steps = steps
self.state = state
def print_progress_table(self, current_step_name):
table = Table(title="ViBe Setup Progress", box=box.ROUNDED)
table.add_column("#", justify="center")
table.add_column("Step", justify="left")
table.add_column("Description", justify="left")
table.add_column("Status", justify="center")
for index, step in enumerate(self.steps, 1):
if self.state.get(step.name):
status = "✅"
elif step.name == current_step_name:
status = "🔄"
else:
status = "⏳"
table.add_row(str(index), step.name, step.description, status)
console.print(table)
def run(self):
serializable_steps = [{"name": step.name, "description": step.description} for step in self.steps]
self.state.update("steps", serializable_steps)
for step in self.steps:
if step.should_run(self.state):
clear_screen()
self.print_progress_table(step.name)
if step.instructions:
step.display_instructions()
step.run(self.state)
clear_screen()
self.print_progress_table("done")
console.print("\n[bold green]🎉 Setup completed![/bold green]")
console.print("\n[bold blue]👉 Run `pnpm run dev` in the backend and frontend directories to start the servers.[/bold blue]")
# ------------------ Main ------------------
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--summary":
state = SetupState()
state.show_summary()
return
backend_dir = os.path.join(os.getcwd(), "backend")
frontend_dir = os.path.join(os.getcwd(), "frontend")
state = SetupState()
development_steps = [
ToolchainCheckStep(),
FirebaseLoginStep(),
FirebaseEmulatorsStep(backend_dir),
EnvFileStep(backend_dir),
PackageInstallStep(backend_dir),
MongoDBBinaryStep(backend_dir),
TestStep(backend_dir),
FrontendPackageInstallStep(frontend_dir)
]
welcome_step = WelcomeStep()
environment = welcome_step.run(state)
if environment == "Development":
development_pipeline = SetupPipeline(development_steps, state)
development_pipeline.run()
elif environment == "Production":
console.print("[red]Production setup is not ready yet.[/red]")
sys.exit(1)
console.print("\n[bold cyan]Setup Summary[/bold cyan]")
if __name__ == "__main__":
main()