astris-python 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.
astris/installer.py ADDED
@@ -0,0 +1,595 @@
1
+ import io
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ if sys.platform == "win32":
9
+ if isinstance(sys.stdout, io.TextIOWrapper):
10
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
11
+ if isinstance(sys.stderr, io.TextIOWrapper):
12
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
13
+
14
+ installer_cli = typer.Typer(
15
+ name="astris",
16
+ help="Astris Framework Installer",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ invoke_without_command=True,
20
+ )
21
+
22
+
23
+ @installer_cli.callback()
24
+ def callback():
25
+ """Astris Framework Installer."""
26
+
27
+
28
+ @installer_cli.command()
29
+ def new(
30
+ name: str = typer.Argument(..., help="The name of the new project directory"),
31
+ local_path: str | None = typer.Option(
32
+ None, "--local", "-l", help="Path to local Astris repo for development"
33
+ ),
34
+ auth: bool = typer.Option(
35
+ True,
36
+ "--auth/--no-auth",
37
+ help="Scaffold full-stack authentication starter kit (default: enabled)",
38
+ ),
39
+ ):
40
+ """Scaffold a brand-new Astris full-stack project."""
41
+ project_dir = Path.cwd() / name
42
+
43
+ if project_dir.exists():
44
+ typer.secho(f"Error: Directory '{name}' already exists!", fg=typer.colors.RED)
45
+ raise typer.Exit(1)
46
+
47
+ typer.secho(
48
+ f"šŸš€ Crafting your Astris application: {name}", fg=typer.colors.CYAN, bold=True
49
+ )
50
+
51
+ # 1. Directory layout (Domain Driven + Full-Stack Resources)
52
+ core_dir = project_dir / "app" / "core"
53
+ shared_dir = project_dir / "app" / "shared"
54
+ modules_dir = project_dir / "app" / "modules"
55
+ welcome_module_dir = modules_dir / "welcome"
56
+ database_dir = project_dir / "database" / "migrations"
57
+ public_dir = project_dir / "public"
58
+ views_dir = project_dir / "resources" / "views"
59
+ css_dir = project_dir / "resources" / "css"
60
+ js_dir = project_dir / "resources" / "js"
61
+ pages_dir = js_dir / "Pages"
62
+ components_dir = js_dir / "Components"
63
+
64
+ for directory in [
65
+ core_dir,
66
+ shared_dir,
67
+ welcome_module_dir,
68
+ database_dir,
69
+ public_dir,
70
+ views_dir,
71
+ css_dir,
72
+ js_dir,
73
+ pages_dir,
74
+ components_dir,
75
+ ]:
76
+ directory.mkdir(parents=True, exist_ok=True)
77
+
78
+ (project_dir / "app" / "__init__.py").touch()
79
+ (core_dir / "__init__.py").touch()
80
+ (shared_dir / "__init__.py").touch()
81
+ (modules_dir / "__init__.py").touch()
82
+ (welcome_module_dir / "__init__.py").touch()
83
+
84
+ # Default framework favicon
85
+ try:
86
+ import importlib.resources as pkg_resources
87
+
88
+ favicon_bytes = (
89
+ pkg_resources.files("astris.assets").joinpath("favicon.ico").read_bytes()
90
+ )
91
+ (public_dir / "favicon.ico").write_bytes(favicon_bytes)
92
+ except (ImportError, OSError, TypeError):
93
+ local_favicon = Path(__file__).parent / "assets" / "favicon.ico"
94
+ if local_favicon.exists():
95
+ (public_dir / "favicon.ico").write_bytes(local_favicon.read_bytes())
96
+
97
+ from astris.database.migrations import ensure_migration_setup
98
+
99
+ ensure_migration_setup(project_dir)
100
+
101
+ # 1b. Centralized settings: app/core/config.py
102
+ config_stub = """from astris.config import Settings as BaseAppSettings
103
+
104
+
105
+ class Settings(BaseAppSettings):
106
+ \"\"\"Extend application settings with custom environment variables.\"\"\"
107
+
108
+ # Add custom settings here (e.g. STRIPE_KEY, REDIS_URL, etc.)
109
+ pass
110
+
111
+
112
+ settings = Settings()
113
+ """
114
+ (core_dir / "config.py").write_text(config_stub, encoding="utf-8")
115
+
116
+ import secrets
117
+
118
+ # 2. Environment configuration (.env and .env.example)
119
+ app_key = secrets.token_urlsafe(32)
120
+ env_content = f"""APP_NAME={name}
121
+ APP_ENV=local
122
+ APP_DEBUG=true
123
+ APP_KEY={app_key}
124
+
125
+ DATABASE_URL=sqlite:///database/app.db
126
+ """
127
+ env_example_content = f"""APP_NAME={name}
128
+ APP_ENV=local
129
+ APP_DEBUG=true
130
+ APP_KEY=
131
+
132
+ DATABASE_URL=sqlite:///database/app.db
133
+ """
134
+ (project_dir / ".env").write_text(env_content, encoding="utf-8")
135
+ (project_dir / ".env.example").write_text(env_example_content, encoding="utf-8")
136
+
137
+ # 3. pyproject.toml
138
+ pyproject_content = f'''[project]
139
+ name = "{name.lower().replace("_", "-")}"
140
+ version = "0.1.0"
141
+ description = "An Astris web application"
142
+ readme = "README.md"
143
+ requires-python = ">=3.11"
144
+ dependencies = []
145
+ '''
146
+ (project_dir / "pyproject.toml").write_text(pyproject_content, encoding="utf-8")
147
+ project_readme = f"""<p align="center" style="padding: 20px 0 10px 0;">
148
+ <a href="https://github.com/TheFelixGomez/astris">
149
+ <img src="https://raw.githubusercontent.com/TheFelixGomez/astris/main/.github/assets/astris-logo-name.png" alt="Astris" width="380">
150
+ </a>
151
+ </p>
152
+
153
+ <p align="center">
154
+ <strong>The modern full-stack web framework for Python.</strong><br>
155
+ Full-stack simplicity with modern Python performance.
156
+ </p>
157
+
158
+ ---
159
+
160
+ # {name}
161
+
162
+ An application built with **Astris**.
163
+
164
+ ---
165
+
166
+ ## šŸš€ Getting Started
167
+
168
+ ### 1. Install Frontend Dependencies
169
+ ```bash
170
+ npm install
171
+ ```
172
+
173
+ ### 2. Start Development Server
174
+ Launch the full-stack development server:
175
+ ```bash
176
+ uv run orbit serve
177
+ ```
178
+ Open **`http://localhost:8000`** in your browser.
179
+
180
+ ---
181
+
182
+ ## 🪐 Orbit CLI Commands
183
+
184
+ | Command | Description |
185
+ | :--- | :--- |
186
+ | `uv run orbit serve` | Start full-stack development server with hot-reloading |
187
+ | `uv run orbit make:module <name>` | Scaffold a complete domain module (Controller, Model, View) |
188
+ | `uv run orbit make:controller <name>` | Generate an Astris controller |
189
+ | `uv run orbit make:model <name>` | Generate a database model |
190
+ | `uv run orbit migrate` | Run all pending database migrations |
191
+ | `uv run orbit make:migration "<message>"` | Auto-generate a new database schema migration |
192
+ | `uv run orbit key:generate` | Generate a new 32-byte secret `APP_KEY` in `.env` |
193
+
194
+ ---
195
+
196
+ ## šŸ’” Project Architecture
197
+
198
+ * **`app/modules/`**: Domain modules with controllers, models, and routes.
199
+ * **`app/core/config.py`**: Centralized application configuration.
200
+ * **`resources/js/Pages/`**: Frontend single-page application views.
201
+ * **`database/migrations/`**: Database schema migrations managed by Orbit.
202
+ * **`public/`**: Web root directory for static assets (favicon, images, robots.txt).
203
+
204
+ ---
205
+
206
+ ## šŸ“– About Astris
207
+
208
+ **Astris** is a modern full-stack web framework for Python with expressive, type-safe elegance. Designed to help developers build and ship modern web applications with speed and simplicity.
209
+
210
+ * **Repository**: [https://github.com/TheFelixGomez/astris](https://github.com/TheFelixGomez/astris)
211
+ * **Author**: Felix Gomez ([@TheFelixGomez](https://github.com/TheFelixGomez))
212
+ """
213
+ (project_dir / "README.md").write_text(project_readme, encoding="utf-8")
214
+ (project_dir / ".gitignore").write_text(
215
+ ".venv/\nnode_modules/\n__pycache__/\n*.pyc\n.env\npublic/build/\n",
216
+ encoding="utf-8",
217
+ )
218
+
219
+ # 3. main.py entry point
220
+ main_content = """from astris import Astris
221
+
222
+ app = Astris()
223
+ """
224
+ (project_dir / "main.py").write_text(main_content, encoding="utf-8")
225
+
226
+ # 4. Default Welcome Controller using InertiaResponse
227
+ welcome_controller = """from astris.http import Request
228
+ from astris.inertia import InertiaResponse
229
+ from astris.routing import Controller
230
+
231
+ controller = Controller(tags=["Home"])
232
+
233
+
234
+ @controller.get("/")
235
+ async def index(request: Request) -> InertiaResponse:
236
+ return InertiaResponse(
237
+ request,
238
+ "Welcome",
239
+ props={
240
+ "status": "online",
241
+ "message": "Welcome to your Astris application! šŸš€",
242
+ "version": "0.1.0",
243
+ "api_docs_url": "/docs",
244
+ "redoc_url": "/redoc",
245
+ "docs_url": "https://astris.dev/docs",
246
+ },
247
+ )
248
+ """
249
+ (welcome_module_dir / "welcome_controller.py").write_text(
250
+ welcome_controller, encoding="utf-8"
251
+ )
252
+
253
+ # 5. Frontend: package.json
254
+ package_json_content = f'''{{
255
+ "name": "{name.lower().replace("_", "-")}",
256
+ "private": true,
257
+ "type": "module",
258
+ "scripts": {{
259
+ "dev": "vite",
260
+ "build": "vite build"
261
+ }},
262
+ "dependencies": {{
263
+ "@inertiajs/vue3": "^3.6.1",
264
+ "vue": "^3.5.41"
265
+ }},
266
+ "devDependencies": {{
267
+ "@tailwindcss/vite": "^4.3.3",
268
+ "@vitejs/plugin-vue": "^6.0.8",
269
+ "tailwindcss": "^4.3.3",
270
+ "typescript": "^7.0.2",
271
+ "vite": "^8.2.2"
272
+ }}
273
+ }}
274
+ '''
275
+ (project_dir / "package.json").write_text(package_json_content, encoding="utf-8")
276
+
277
+ # 6. Frontend: vite.config.ts
278
+ vite_config_content = """import { defineConfig } from "vite";
279
+ import vue from "@vitejs/plugin-vue";
280
+ import tailwindcss from "@tailwindcss/vite";
281
+ import { resolve } from "path";
282
+
283
+ export default defineConfig({
284
+ plugins: [tailwindcss(), vue()],
285
+ publicDir: false,
286
+ resolve: {
287
+ alias: {
288
+ "@": resolve(import.meta.dirname, "resources/js"),
289
+ },
290
+ },
291
+ build: {
292
+ outDir: "public/build",
293
+ emptyOutDir: true,
294
+ manifest: true,
295
+ rollupOptions: {
296
+ input: "resources/js/app.ts",
297
+ },
298
+ },
299
+ server: {
300
+ origin: "http://localhost:5173",
301
+ port: 5173,
302
+ strictPort: true,
303
+ },
304
+ });
305
+ """
306
+ (project_dir / "vite.config.ts").write_text(vite_config_content, encoding="utf-8")
307
+
308
+ # 7. Frontend: tsconfig.json
309
+ tsconfig_content = """{
310
+ "compilerOptions": {
311
+ "target": "ESNext",
312
+ "useDefineForClassFields": true,
313
+ "module": "ESNext",
314
+ "moduleResolution": "Bundler",
315
+ "strict": true,
316
+ "jsx": "preserve",
317
+ "resolveJsonModule": true,
318
+ "isolatedModules": true,
319
+ "esModuleInterop": true,
320
+ "lib": ["ESNext", "DOM"],
321
+ "skipLibCheck": true,
322
+ "baseUrl": ".",
323
+ "paths": {
324
+ "@/*": ["resources/js/*"]
325
+ }
326
+ },
327
+ "include": ["resources/js/**/*.ts", "resources/js/**/*.d.ts", "resources/js/**/*.vue"]
328
+ }
329
+ """
330
+ (project_dir / "tsconfig.json").write_text(tsconfig_content, encoding="utf-8")
331
+
332
+ # 8. Frontend: resources/css/app.css (Tailwind CSS v4)
333
+ (css_dir / "app.css").write_text('@import "tailwindcss";\n', encoding="utf-8")
334
+
335
+ # 9. Frontend: resources/views/root.html
336
+ root_html_content = """<!DOCTYPE html>
337
+ <html lang="en">
338
+ <head>
339
+ <meta charset="UTF-8">
340
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
341
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
342
+ <title>Astris Application</title>
343
+ </head>
344
+ <body class="bg-slate-950 text-slate-100 antialiased font-sans">
345
+ @inertia
346
+
347
+ @vite
348
+ </body>
349
+ </html>
350
+ """
351
+ (views_dir / "root.html").write_text(root_html_content, encoding="utf-8")
352
+
353
+ # 10. Frontend: resources/js/app.ts
354
+ app_ts_content = """import "../css/app.css";
355
+ import { createApp, h } from "vue";
356
+ import { createInertiaApp } from "@inertiajs/vue3";
357
+
358
+ const el = document.getElementById("app");
359
+
360
+ if (!el || !el.dataset.page) {
361
+ throw new Error("Inertia root element (#app) or data-page attribute not found in the DOM.");
362
+ }
363
+
364
+ const initialPage = JSON.parse(el.dataset.page);
365
+ const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
366
+
367
+ createInertiaApp({
368
+ page: initialPage,
369
+ resolve: (name) => {
370
+ const page: any = pages[`./Pages/${name}.vue`];
371
+ if (!page) {
372
+ throw new Error(`Page component "${name}" not found in ./Pages/`);
373
+ }
374
+ return page.default ?? page;
375
+ },
376
+ setup({ el, App, props, plugin }) {
377
+ createApp({ render: () => h(App, props) })
378
+ .use(plugin)
379
+ .mount(el);
380
+ },
381
+ });
382
+ """
383
+ (js_dir / "app.ts").write_text(app_ts_content, encoding="utf-8")
384
+
385
+ # 10. Frontend: resources/js/Components/AstrisLogo.vue
386
+ astris_logo_vue_content = """<template>
387
+ <svg
388
+ viewBox="0 0 792 792"
389
+ fill="currentColor"
390
+ xmlns="http://www.w3.org/2000/svg"
391
+ >
392
+ <path d="m50 757h88.8l37.8-92.3c-31.4-1.6-59-6.3-82.4-14.2z"/>
393
+ <path d="m596.3 550.8q-11 6.5-22.3 12.6l79.1 193.6h88.9l-98.1-236.5q-22.4 15.6-47.6 30.3z"/>
394
+ <path d="m210.1 582.6l185.9-454.7 147.5 360.9c25-14.4 48-29.9 68.2-46l-169.2-407.8h-93l-221.6 534.1c21.2 8.3 49.5 12.9 82.2 13.5z"/>
395
+ <path fill-rule="evenodd" d="m372.5 466.8l23.5 76.4 23.5-76.4 69-23.5-68.8-23.8-23.7-75.8-23.7 75.8-68.8 23.8 69 23.5z"/>
396
+ <path d="m756.2 309.9c-18.3-50.9-96.5-72.1-201.4-61.6 81.6-3.1 141.7 15.8 156.8 58 26.6 74.2-96.3 192.3-273.9 256.1-177.7 63.7-343.3 55.2-370-19-15.5-43.4 19.7-99.6 87.1-151.5-90.4 60.9-137.6 131.1-118.9 183.2 29.4 82 214.3 90.6 413.1 19.3 198.8-71.3 336.6-202.5 307.2-284.5z"/>
397
+ <path d="m90 533.2c2.1 6 5.3 11.5 9.4 16.6q-2.3-3.9-3.9-8.2c-12.7-35.5 18.2-82.5 77.1-127.1l12.8-31c-71.7 50.7-110.7 107.1-95.4 149.7z"/>
398
+ <path d="m437 275.7c-30 6.8-61.1 15.9-92.6 27.2q-0.3 0.1-0.6 0.2l-10.1 24.7q8.5-3.3 17.3-6.4c31.8-11.4 63.2-20.7 93.3-27.8l-7.4-17.9z"/>
399
+ <path d="m688.7 323.7q1.4 4.1 2.1 8.4c0-6.6-1.1-12.9-3.2-18.9-13.1-36.3-63.2-53.7-132.2-52.9l7 16.8c66.6-1.8 114.4 13.4 126.3 46.6z"/>
400
+ </svg>
401
+ </template>
402
+ """
403
+ (components_dir / "AstrisLogo.vue").write_text(
404
+ astris_logo_vue_content, encoding="utf-8"
405
+ )
406
+
407
+ # 11. Frontend: resources/js/Pages/Welcome.vue
408
+ welcome_vue_content = """<script setup lang="ts">
409
+ import { Link, usePage } from '@inertiajs/vue3';
410
+ import AstrisLogo from '../Components/AstrisLogo.vue';
411
+
412
+ interface Props {
413
+ status: string;
414
+ message: string;
415
+ version?: string;
416
+ api_docs_url?: string;
417
+ redoc_url?: string;
418
+ docs_url?: string;
419
+ }
420
+
421
+ withDefaults(defineProps<Props>(), {
422
+ version: "0.1.0",
423
+ api_docs_url: "/docs",
424
+ redoc_url: "/redoc",
425
+ docs_url: "https://github.com/TheFelixGomez/astris",
426
+ });
427
+
428
+ const page = usePage();
429
+ </script>
430
+
431
+ <template>
432
+ <main class="min-h-screen flex flex-col justify-between bg-slate-950 text-slate-100 p-6 font-sans relative overflow-hidden selection:bg-sky-500 selection:text-white">
433
+ <!-- Subtle Background Ambient Glow -->
434
+ <div class="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[350px] bg-sky-500/10 blur-[120px] rounded-full pointer-events-none -z-10"></div>
435
+
436
+ <!-- Top Navigation -->
437
+ <header class="flex justify-between items-center max-w-5xl w-full mx-auto py-2">
438
+ <div class="flex items-center gap-2.5">
439
+ <AstrisLogo class="w-8 h-8 text-sky-400" />
440
+ <span class="font-bold text-lg tracking-tight text-white">Astris</span>
441
+ </div>
442
+
443
+ <nav class="flex items-center gap-3">
444
+ <template v-if="page.props.auth?.user">
445
+ <Link
446
+ href="/dashboard"
447
+ class="px-4 py-2 rounded-xl text-sm font-medium text-sky-400 bg-sky-500/10 border border-sky-500/20 hover:bg-sky-500/20 hover:border-sky-500/40 transition duration-200"
448
+ >
449
+ Dashboard &rarr;
450
+ </Link>
451
+ </template>
452
+ <template v-else>
453
+ <Link
454
+ href="/login"
455
+ class="px-3.5 py-1.5 rounded-xl text-sm font-medium text-slate-300 hover:text-white transition duration-200"
456
+ >
457
+ Sign In
458
+ </Link>
459
+ <Link
460
+ href="/register"
461
+ class="px-4 py-1.5 rounded-xl text-sm font-medium text-white bg-sky-500 hover:bg-sky-400 shadow-md shadow-sky-500/20 transition duration-200"
462
+ >
463
+ Register
464
+ </Link>
465
+ </template>
466
+ </nav>
467
+ </header>
468
+
469
+ <!-- Main Hero Content -->
470
+ <div class="max-w-3xl w-full my-auto mx-auto text-center py-10">
471
+ <!-- Hero Logo & Title -->
472
+ <div class="mb-10 flex flex-col items-center">
473
+ <div class="relative mb-6 group">
474
+ <div class="absolute -inset-2 bg-gradient-to-r from-sky-500 to-indigo-500 rounded-3xl blur-lg opacity-30 group-hover:opacity-60 transition duration-500"></div>
475
+ <div class="relative p-4 rounded-2xl bg-slate-900/80 border border-slate-800 backdrop-blur shadow-2xl">
476
+ <AstrisLogo class="w-16 h-16 text-sky-400" />
477
+ </div>
478
+ </div>
479
+
480
+ <h1 class="text-4xl sm:text-5xl font-extrabold tracking-tight text-white mb-3">
481
+ Astris
482
+ </h1>
483
+ <p class="text-lg sm:text-xl text-slate-400 max-w-xl mx-auto leading-relaxed mb-5">
484
+ {{ message }}
485
+ </p>
486
+
487
+ <!-- Version & Status Badge -->
488
+ <div class="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-slate-900/80 border border-slate-800 text-xs font-mono text-slate-300 shadow-sm backdrop-blur">
489
+ <span class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_#34d399] animate-pulse"></span>
490
+ <span>Status: <span class="text-emerald-400 font-semibold">{{ status }}</span></span>
491
+ <span class="text-slate-600">&bull;</span>
492
+ <span class="text-sky-400">v{{ version }}</span>
493
+ </div>
494
+ </div>
495
+
496
+ <!-- Quick Navigation Cards -->
497
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 text-left mb-10">
498
+ <!-- Interactive API Docs -->
499
+ <a
500
+ :href="api_docs_url"
501
+ target="_blank"
502
+ rel="noopener noreferrer"
503
+ class="group p-5 rounded-2xl bg-slate-900/60 hover:bg-slate-900/90 border border-slate-800 hover:border-sky-500/40 backdrop-blur shadow-lg transition-all duration-200 hover:-translate-y-1"
504
+ >
505
+ <div class="w-9 h-9 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400 text-lg mb-3.5 group-hover:scale-110 transition duration-200">
506
+ ⚔
507
+ </div>
508
+ <h3 class="text-sm font-semibold text-white mb-1 group-hover:text-sky-400 transition">Swagger API Docs</h3>
509
+ <p class="text-xs text-slate-400 leading-relaxed">Interactive OpenAPI interface to explore and test endpoints.</p>
510
+ </a>
511
+
512
+ <!-- ReDoc API Docs -->
513
+ <a
514
+ :href="redoc_url"
515
+ target="_blank"
516
+ rel="noopener noreferrer"
517
+ class="group p-5 rounded-2xl bg-slate-900/60 hover:bg-slate-900/90 border border-slate-800 hover:border-sky-500/40 backdrop-blur shadow-lg transition-all duration-200 hover:-translate-y-1"
518
+ >
519
+ <div class="w-9 h-9 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400 text-lg mb-3.5 group-hover:scale-110 transition duration-200">
520
+ šŸ“‘
521
+ </div>
522
+ <h3 class="text-sm font-semibold text-white mb-1 group-hover:text-sky-400 transition">ReDoc Schema</h3>
523
+ <p class="text-xs text-slate-400 leading-relaxed">Clean, structured documentation for API schemas and models.</p>
524
+ </a>
525
+
526
+ <!-- Project / Framework Documentation -->
527
+ <a
528
+ :href="docs_url"
529
+ target="_blank"
530
+ rel="noopener noreferrer"
531
+ class="group p-5 rounded-2xl bg-slate-900/60 hover:bg-slate-900/90 border border-slate-800 hover:border-sky-500/40 backdrop-blur shadow-lg transition-all duration-200 hover:-translate-y-1"
532
+ >
533
+ <div class="w-9 h-9 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400 text-lg mb-3.5 group-hover:scale-110 transition duration-200">
534
+ šŸ“–
535
+ </div>
536
+ <h3 class="text-sm font-semibold text-white mb-1 group-hover:text-sky-400 transition">Astris Docs</h3>
537
+ <p class="text-xs text-slate-400 leading-relaxed">Official guides, controllers, Inertia integration, and tutorials.</p>
538
+ </a>
539
+ </div>
540
+
541
+ <!-- Quick Command Tip -->
542
+ <div class="p-3.5 rounded-xl bg-slate-900/70 border border-slate-800/80 text-xs font-mono text-slate-400 flex items-center justify-center gap-2 shadow-inner">
543
+ <span>Get started:</span>
544
+ <code class="text-sky-400 bg-slate-950 px-2 py-0.5 rounded border border-slate-800">uv run orbit make:module billing</code>
545
+ <span class="text-slate-600">&bull;</span>
546
+ <code class="text-indigo-400 bg-slate-950 px-2 py-0.5 rounded border border-slate-800">uv run orbit serve</code>
547
+ </div>
548
+ </div>
549
+
550
+ <!-- Footer -->
551
+ <footer class="text-center text-xs text-slate-500 py-4">
552
+ Built with ā¤ļø by <a href="https://github.com/TheFelixGomez" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-slate-300 underline underline-offset-4 transition">Felix Gomez</a>.
553
+ </footer>
554
+ </main>
555
+ </template>
556
+ """
557
+ (pages_dir / "Welcome.vue").write_text(welcome_vue_content, encoding="utf-8")
558
+
559
+ if auth:
560
+ from astris.auth.installer import install_auth_starter
561
+
562
+ install_auth_starter(project_dir)
563
+ typer.secho(
564
+ "āœ“ Scaffolded full-stack authentication starter kit", fg=typer.colors.GREEN
565
+ )
566
+
567
+ # 11. Initialize Python virtualenv and resolve dependencies
568
+ typer.echo("šŸ“¦ Initializing virtual environment and resolving dependencies...")
569
+ subprocess.run(["uv", "venv"], cwd=project_dir, check=True)
570
+
571
+ if local_path:
572
+ resolved_local = str(Path(local_path).resolve())
573
+ subprocess.run(["uv", "add", resolved_local], cwd=project_dir, check=True)
574
+ else:
575
+ sibling_astris = (Path.cwd() / "astris").resolve()
576
+ parent_astris = (Path.cwd() / ".." / "astris").resolve()
577
+
578
+ if (sibling_astris / "pyproject.toml").exists():
579
+ subprocess.run(
580
+ ["uv", "add", str(sibling_astris)], cwd=project_dir, check=True
581
+ )
582
+ elif (parent_astris / "pyproject.toml").exists():
583
+ subprocess.run(
584
+ ["uv", "add", str(parent_astris)], cwd=project_dir, check=True
585
+ )
586
+ else:
587
+ subprocess.run(["uv", "add", "astris-python"], cwd=project_dir, check=True)
588
+
589
+ typer.secho(
590
+ f"\nāœ“ Project {name} created successfully!", fg=typer.colors.GREEN, bold=True
591
+ )
592
+ typer.echo("\nTo get started, run:")
593
+ typer.secho(f" cd {name}", fg=typer.colors.YELLOW)
594
+ typer.secho(" npm install", fg=typer.colors.YELLOW)
595
+ typer.secho(" uv run orbit serve\n", fg=typer.colors.YELLOW)