spawnio 1.0.5__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 (56) hide show
  1. spawn/__init__.py +6 -0
  2. spawn/cli/__init__.py +0 -0
  3. spawn/cli/app.py +218 -0
  4. spawn/cli/noninteractive.py +204 -0
  5. spawn/cli/prompts.py +267 -0
  6. spawn/core/__init__.py +0 -0
  7. spawn/core/exceptions.py +7 -0
  8. spawn/core/models.py +21 -0
  9. spawn/core/registry.py +152 -0
  10. spawn/generators/__init__.py +0 -0
  11. spawn/generators/custom_structure.py +508 -0
  12. spawn/generators/project_generator.py +115 -0
  13. spawn/github/__init__.py +0 -0
  14. spawn/github/exceptions.py +5 -0
  15. spawn/github/publisher.py +69 -0
  16. spawn/github/validators.py +23 -0
  17. spawn/templates/__init__.py +0 -0
  18. spawn/templates/agent/__init__.py +182 -0
  19. spawn/templates/agent/content.py +374 -0
  20. spawn/templates/automation/__init__.py +100 -0
  21. spawn/templates/automation/content.py +181 -0
  22. spawn/templates/backend_api/__init__.py +243 -0
  23. spawn/templates/backend_api/content.py +509 -0
  24. spawn/templates/base.py +58 -0
  25. spawn/templates/chatbot/__init__.py +243 -0
  26. spawn/templates/chatbot/content.py +668 -0
  27. spawn/templates/cli_application/__init__.py +211 -0
  28. spawn/templates/cli_application/content.py +512 -0
  29. spawn/templates/data_project/__init__.py +125 -0
  30. spawn/templates/data_project/analysis/__init__.py +74 -0
  31. spawn/templates/data_project/analysis/content.py +190 -0
  32. spawn/templates/data_project/dashboard/__init__.py +61 -0
  33. spawn/templates/data_project/dashboard/content.py +135 -0
  34. spawn/templates/data_project/etl/__init__.py +66 -0
  35. spawn/templates/data_project/etl/content.py +239 -0
  36. spawn/templates/data_project/ml/__init__.py +62 -0
  37. spawn/templates/data_project/ml/content.py +239 -0
  38. spawn/templates/mcp_server/__init__.py +83 -0
  39. spawn/templates/mcp_server/content.py +161 -0
  40. spawn/templates/rag/__init__.py +124 -0
  41. spawn/templates/rag/content.py +463 -0
  42. spawn/templates/shared_content.py +89 -0
  43. spawn/utils/__init__.py +0 -0
  44. spawn/utils/banner.py +41 -0
  45. spawn/utils/console.py +3 -0
  46. spawn/utils/doctor.py +594 -0
  47. spawn/utils/git.py +132 -0
  48. spawn/utils/success.py +47 -0
  49. spawn/utils/theme.py +25 -0
  50. spawn/utils/uv.py +61 -0
  51. spawn/utils/validators.py +15 -0
  52. spawnio-1.0.5.dist-info/METADATA +798 -0
  53. spawnio-1.0.5.dist-info/RECORD +56 -0
  54. spawnio-1.0.5.dist-info/WHEEL +4 -0
  55. spawnio-1.0.5.dist-info/entry_points.txt +2 -0
  56. spawnio-1.0.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,463 @@
1
+ INIT_CONTENT = ""
2
+
3
+ # ─── Knowledge base ───────────────────────────────────────────────────────
4
+
5
+ SAMPLE_KNOWLEDGE_CONTENT = """\
6
+ # Spawn Knowledge Base
7
+
8
+ ## What is Spawn?
9
+
10
+ Spawn is an intent-based project generator that helps developers create
11
+ production-friendly project foundations in seconds. Instead of manually
12
+ creating folders, installing dependencies, and configuring tools, developers
13
+ run one command and get a complete, immediately runnable project.
14
+
15
+ ## Spawn Intents
16
+
17
+ Spawn supports the following project intents:
18
+
19
+ - Backend API: FastAPI, Flask, or Django with production structure
20
+ - CLI Application: Typer, Click, or Argparse command-line tools
21
+ - Automation Tool: Workflow-based automation with logging
22
+ - AI Chatbot: Conversational AI with PydanticAI, OpenAI SDK, or LiteLLM
23
+ - AI Agent: Tool-calling agents with PydanticAI or OpenAI Agents SDK
24
+ - RAG System: Retrieval-Augmented Generation with LlamaIndex and ChromaDB
25
+
26
+ ## How Spawn Works
27
+
28
+ Run `spawn create`, select your intent, choose your framework and provider,
29
+ pick any extras (ruff, pytest, github-actions), and Spawn generates the full
30
+ project structure, installs dependencies via uv, and shows you the exact
31
+ commands to run next. The entire setup takes under 30 seconds.
32
+
33
+ ## RAG System Intent
34
+
35
+ The RAG System intent (added in v0.8.0) generates a complete
36
+ Retrieval-Augmented Generation application using LlamaIndex and ChromaDB.
37
+ It ingests documents from the data/ directory, creates embeddings using
38
+ OpenAI's text-embedding-3-small model, stores them in a local ChromaDB
39
+ vector database, and answers questions using retrieved context. On first
40
+ run the ingestion step is triggered automatically before the Q&A loop starts.
41
+
42
+ ## Version History
43
+
44
+ - v0.6.0: Initial release with Backend API, CLI Application, Automation Tool,
45
+ and AI Chatbot intents.
46
+ - v0.7.0: Added AI Agent intent with PydanticAI and OpenAI Agents SDK support,
47
+ plus Groq provider across all AI intents.
48
+ - v0.8.0: Added RAG System intent with LlamaIndex, ChromaDB, and OpenAI.
49
+ """
50
+
51
+ # ─── Config ───────────────────────────────────────────────────────────────
52
+
53
+ SETTINGS_CONTENT = """\
54
+ import os
55
+ from pathlib import Path
56
+
57
+ from dotenv import load_dotenv
58
+ from llama_index.core import Settings
59
+ from llama_index.embeddings.openai import OpenAIEmbedding
60
+ from llama_index.llms.openai import OpenAI
61
+
62
+
63
+ def load_env() -> None:
64
+ load_dotenv()
65
+
66
+
67
+ def configure_llm() -> None:
68
+ api_key = os.getenv("OPENAI_API_KEY", "")
69
+ model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
70
+ Settings.llm = OpenAI(model=model, api_key=api_key)
71
+ Settings.embed_model = OpenAIEmbedding(
72
+ model="text-embedding-3-small",
73
+ api_key=api_key,
74
+ )
75
+ """
76
+
77
+ # ─── Knowledge index ──────────────────────────────────────────────────────
78
+
79
+ KNOWLEDGE_INDEX_CONTENT = """\
80
+ import re
81
+ from pathlib import Path
82
+
83
+ import chromadb
84
+ from llama_index.core import StorageContext
85
+ from llama_index.vector_stores.chroma import ChromaVectorStore
86
+
87
+
88
+ def _sanitize_collection_name(name: str) -> str:
89
+ cleaned = re.sub(r"[^a-zA-Z0-9._-]", "-", name)
90
+ cleaned = cleaned.strip("-._")
91
+ if len(cleaned) < 3:
92
+ cleaned = f"rag-{{cleaned}}".strip("-._")
93
+ if len(cleaned) < 3:
94
+ cleaned = "rag-collection"
95
+ return cleaned[:512]
96
+
97
+
98
+ CHROMA_PATH = Path("chroma_db")
99
+ COLLECTION_NAME = _sanitize_collection_name("{project_name}")
100
+
101
+
102
+ def get_vector_store() -> ChromaVectorStore:
103
+ client = chromadb.PersistentClient(path=str(CHROMA_PATH))
104
+ collection = client.get_or_create_collection(COLLECTION_NAME)
105
+ return ChromaVectorStore(chroma_collection=collection)
106
+
107
+
108
+ def get_storage_context() -> StorageContext:
109
+ return StorageContext.from_defaults(vector_store=get_vector_store())
110
+
111
+
112
+ def index_exists() -> bool:
113
+ if not CHROMA_PATH.exists():
114
+ return False
115
+ sqlite_file = CHROMA_PATH / "chroma.sqlite3"
116
+ return sqlite_file.exists() and sqlite_file.stat().st_size > 0
117
+ """
118
+
119
+ # ─── Ingestion ────────────────────────────────────────────────────────────
120
+
121
+ INGESTION_CONTENT = """\
122
+ from pathlib import Path
123
+
124
+ from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
125
+ from openai import APIError, AuthenticationError, RateLimitError
126
+
127
+ from src.knowledge.index import get_storage_context
128
+
129
+ DATA_PATH = Path("data")
130
+
131
+
132
+ def ingest_documents() -> VectorStoreIndex:
133
+ print("Ingesting documents from data/...")
134
+ documents = SimpleDirectoryReader(
135
+ input_dir=str(DATA_PATH),
136
+ required_exts=[".txt", ".md"],
137
+ recursive=True,
138
+ ).load_data()
139
+ print(f"Loaded {{len(documents)}} document(s).")
140
+ storage_context = get_storage_context()
141
+ try:
142
+ index = VectorStoreIndex.from_documents(
143
+ documents,
144
+ storage_context=storage_context,
145
+ show_progress=True,
146
+ )
147
+ except AuthenticationError:
148
+ raise RuntimeError(
149
+ "OpenAI rejected your API key. Check OPENAI_API_KEY in .env."
150
+ ) from None
151
+ except RateLimitError as e:
152
+ if "insufficient_quota" in str(e):
153
+ raise RuntimeError(
154
+ "Your OpenAI account has no available quota/credits. "
155
+ "Check billing at https://platform.openai.com/settings/organization/billing"
156
+ ) from None
157
+ raise RuntimeError(
158
+ "OpenAI rate limit hit. Wait a moment and try again."
159
+ ) from None
160
+ except APIError as e:
161
+ raise RuntimeError(f"OpenAI API error: {{e}}") from None
162
+ print("Ingestion complete. Index stored in chroma_db/")
163
+ return index
164
+ """
165
+
166
+ # ─── Retrieval ────────────────────────────────────────────────────────────
167
+
168
+ RETRIEVAL_CONTENT = """\
169
+ from llama_index.core import VectorStoreIndex
170
+ from openai import APIError, AuthenticationError, RateLimitError
171
+
172
+ from src.knowledge.index import get_storage_context, get_vector_store
173
+
174
+
175
+ def load_index() -> VectorStoreIndex:
176
+ vector_store = get_vector_store()
177
+ storage_context = get_storage_context()
178
+ return VectorStoreIndex.from_vector_store(
179
+ vector_store,
180
+ storage_context=storage_context,
181
+ )
182
+
183
+
184
+ def answer_question(question: str) -> str:
185
+ index = load_index()
186
+ query_engine = index.as_query_engine(
187
+ similarity_top_k=3,
188
+ )
189
+ try:
190
+ response = query_engine.query(question)
191
+ except AuthenticationError:
192
+ raise RuntimeError(
193
+ "OpenAI rejected your API key. Check OPENAI_API_KEY in .env."
194
+ ) from None
195
+ except RateLimitError as e:
196
+ if "insufficient_quota" in str(e):
197
+ raise RuntimeError(
198
+ "Your OpenAI account has no available quota/credits. "
199
+ "Check billing at https://platform.openai.com/settings/organization/billing"
200
+ ) from None
201
+ raise RuntimeError(
202
+ "OpenAI rate limit hit. Wait a moment and try again."
203
+ ) from None
204
+ except APIError as e:
205
+ raise RuntimeError(f"OpenAI API error: {{e}}") from None
206
+ return str(response)
207
+ """
208
+
209
+ # ─── Main ─────────────────────────────────────────────────────────────────
210
+
211
+ MAIN_CONTENT = """\
212
+ from src.config.settings import configure_llm, load_env
213
+ from src.ingestion.ingest import ingest_documents
214
+ from src.knowledge.index import index_exists
215
+ from src.retrieval.retrieve import answer_question
216
+
217
+
218
+ def main() -> None:
219
+ load_env()
220
+ configure_llm()
221
+ print("{project_name} RAG System")
222
+ print("-" * 40)
223
+ if not index_exists():
224
+ print("No index found. Running ingestion...\\n")
225
+ try:
226
+ ingest_documents()
227
+ except RuntimeError as e:
228
+ print(f"\\n[Error] {{e}}\\n")
229
+ import shutil
230
+ from pathlib import Path
231
+ shutil.rmtree(Path("chroma_db"), ignore_errors=True)
232
+ return
233
+ print()
234
+ print("Ask a question (or type 'quit' to exit):\\n")
235
+ while True:
236
+ question = input("You: ").strip()
237
+ if not question:
238
+ continue
239
+ if question.lower() in ("quit", "exit"):
240
+ break
241
+ try:
242
+ answer = answer_question(question)
243
+ print(f"\\nAnswer: {{answer}}\\n")
244
+ except RuntimeError as e:
245
+ print(f"\\n[Error] {{e}}\\n")
246
+ continue
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()
251
+ """
252
+
253
+ # ─── Tests ────────────────────────────────────────────────────────────────
254
+
255
+ CONFTEST_CONTENT = """\
256
+ # conftest.py — shared test fixtures for RAG tests
257
+ """
258
+
259
+ TEST_CONTENT = """\
260
+ from pathlib import Path
261
+ from unittest.mock import MagicMock, patch
262
+
263
+
264
+ def test_index_exists_false_when_no_dir(tmp_path, monkeypatch):
265
+ monkeypatch.chdir(tmp_path)
266
+ from src.knowledge.index import index_exists
267
+ assert not index_exists()
268
+
269
+
270
+ def test_index_exists_false_when_empty_dir(tmp_path, monkeypatch):
271
+ monkeypatch.chdir(tmp_path)
272
+ (tmp_path / "chroma_db").mkdir()
273
+ from src.knowledge.index import index_exists
274
+ assert not index_exists()
275
+
276
+
277
+ def test_index_exists_true_when_has_file(tmp_path, monkeypatch):
278
+ monkeypatch.chdir(tmp_path)
279
+ chroma = tmp_path / "chroma_db"
280
+ chroma.mkdir()
281
+ (chroma / "chroma.sqlite3").write_text("db")
282
+ from src.knowledge.index import index_exists
283
+ assert index_exists()
284
+
285
+
286
+ def test_index_exists_false_when_sqlite_file_empty(tmp_path, monkeypatch):
287
+ monkeypatch.chdir(tmp_path)
288
+ chroma = tmp_path / "chroma_db"
289
+ chroma.mkdir()
290
+ (chroma / "chroma.sqlite3").write_bytes(b"")
291
+ from src.knowledge.index import index_exists
292
+ assert not index_exists()
293
+
294
+
295
+ def test_sample_knowledge_file_exists():
296
+ data_path = Path("data") / "sample_knowledge.md"
297
+ assert data_path.exists(), "data/sample_knowledge.md must exist"
298
+
299
+
300
+ def test_answer_question_mock():
301
+ mock_engine = MagicMock()
302
+ mock_engine.query.return_value = "Spawn is a project generator."
303
+ mock_index = MagicMock()
304
+ mock_index.as_query_engine.return_value = mock_engine
305
+
306
+ with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
307
+ from src.retrieval.retrieve import answer_question
308
+ result = answer_question("What is Spawn?")
309
+
310
+ assert isinstance(result, str)
311
+ assert len(result) > 0
312
+
313
+
314
+ def test_answer_question_rate_limit_raises_runtime_error():
315
+ from openai import RateLimitError
316
+
317
+ mock_engine = MagicMock()
318
+ mock_response = MagicMock()
319
+ mock_response.status_code = 429
320
+ mock_engine.query.side_effect = RateLimitError(
321
+ "insufficient_quota", response=mock_response, body=None
322
+ )
323
+ mock_index = MagicMock()
324
+ mock_index.as_query_engine.return_value = mock_engine
325
+
326
+ with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
327
+ from src.retrieval.retrieve import answer_question
328
+ try:
329
+ answer_question("What is Spawn?")
330
+ assert False, "Expected RuntimeError"
331
+ except RuntimeError as e:
332
+ assert "quota" in str(e).lower() or "rate limit" in str(e).lower()
333
+
334
+
335
+ def test_answer_question_api_error_raises_runtime_error():
336
+ from openai import APIError
337
+
338
+ mock_engine = MagicMock()
339
+ mock_response = MagicMock()
340
+ mock_response.status_code = 500
341
+ mock_engine.query.side_effect = APIError(
342
+ "server error", response=mock_response, body=None
343
+ )
344
+ mock_index = MagicMock()
345
+ mock_index.as_query_engine.return_value = mock_engine
346
+
347
+ with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
348
+ from src.retrieval.retrieve import answer_question
349
+ try:
350
+ answer_question("What is Spawn?")
351
+ assert False, "Expected RuntimeError"
352
+ except RuntimeError as e:
353
+ assert "OpenAI API error" in str(e)
354
+ """
355
+
356
+ # ─── Env ──────────────────────────────────────────────────────────────────
357
+
358
+ ENV_CONTENT = """\
359
+ APP_NAME={project_name}
360
+ OPENAI_API_KEY=
361
+ OPENAI_MODEL=gpt-4o-mini
362
+ """
363
+
364
+ # ─── README ───────────────────────────────────────────────────────────────
365
+
366
+
367
+ def make_readme() -> str:
368
+ return (
369
+ "# {project_name}\n\n"
370
+ "A RAG system generated with Spawn using LlamaIndex and ChromaDB.\n\n"
371
+ "## Getting Started\n\n"
372
+ "1. Rename `.env.example` to `.env`\n\n"
373
+ "2. Add your `OPENAI_API_KEY` to `.env`:\n\n"
374
+ "```env\n"
375
+ "OPENAI_API_KEY=your-key\n"
376
+ "```\n\n"
377
+ "3. (Optional) Add your own `.txt` or `.md` files to `data/`\n\n"
378
+ "4. Run the RAG system:\n\n"
379
+ "```bash\n"
380
+ "uv run python -m src.main\n"
381
+ "```\n\n"
382
+ "On first run, documents in `data/` are automatically ingested.\n"
383
+ "Then ask questions about your knowledge base.\n\n"
384
+ "## Example\n\n"
385
+ "```\n"
386
+ "You: What is Spawn?\n"
387
+ "Answer: Spawn is an intent-based project generator...\n"
388
+ "```\n\n"
389
+ "## Adding Knowledge\n\n"
390
+ "Drop `.txt` or `.md` files into `data/` and delete `chroma_db/` to re-index:\n\n"
391
+ "```bash\n"
392
+ "rm -rf chroma_db/\n"
393
+ "uv run python -m src.main\n"
394
+ "```\n\n"
395
+ "## Project Structure\n\n"
396
+ "```\n"
397
+ "{project_name}/\n"
398
+ "├── data/ # Knowledge documents\n"
399
+ "│ └── sample_knowledge.md\n"
400
+ "├── src/\n"
401
+ "│ ├── config/ # Settings and LLM configuration\n"
402
+ "│ ├── knowledge/ # ChromaDB index management\n"
403
+ "│ ├── ingestion/ # Document ingestion pipeline\n"
404
+ "│ ├── retrieval/ # Query engine and retrieval\n"
405
+ "│ └── main.py\n"
406
+ "├── tests/\n"
407
+ "├── chroma_db/ # Created on first run\n"
408
+ "├── .env.example\n"
409
+ "└── README.md\n"
410
+ "```\n\n"
411
+ "## Running Tests\n\n"
412
+ "```bash\n"
413
+ "uv run pytest\n"
414
+ "```\n"
415
+ )
416
+
417
+
418
+ def make_agents_md() -> str:
419
+ return (
420
+ "# {project_name}\n\n"
421
+ "A RAG system generated with Spawn using LlamaIndex and ChromaDB.\n\n"
422
+ "## Required Environment Variable\n\n"
423
+ "Set `OPENAI_API_KEY` in your `.env` file to run this system:\n\n"
424
+ "```env\n"
425
+ "OPENAI_API_KEY=your-key\n"
426
+ "```\n\n"
427
+ "## Ingestion\n\n"
428
+ "Documents placed in `data/` are automatically ingested into `chroma_db/` on first run.\n"
429
+ )
430
+
431
+ # ─── GitHub Actions ───────────────────────────────────────────────────────
432
+
433
+ GITHUB_ACTIONS_CI_BASE = """\
434
+ name: CI
435
+
436
+ on:
437
+ push:
438
+ branches: [main]
439
+ pull_request:
440
+ branches: [main]
441
+
442
+ jobs:
443
+ ci:
444
+ runs-on: ubuntu-latest
445
+
446
+ steps:
447
+ - uses: actions/checkout@v4
448
+
449
+ - uses: astral-sh/setup-uv@v5
450
+
451
+ - name: Install dependencies
452
+ run: uv sync
453
+ """
454
+
455
+ GITHUB_ACTIONS_CI_RUFF_STEP = """\
456
+ - name: Lint
457
+ run: uv run ruff check .
458
+ """
459
+
460
+ GITHUB_ACTIONS_CI_PYTEST_STEP = """\
461
+ - name: Test
462
+ run: uv run pytest
463
+ """
@@ -0,0 +1,89 @@
1
+ README_CONTENT = """# {project_name}
2
+
3
+ Project generated with Spawn.
4
+ """
5
+
6
+ GITIGNORE_CONTENT = """# Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.pyo
10
+ *.pyd
11
+ *.pyc
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Distribution / packaging
19
+ dist/
20
+ build/
21
+ *.egg-info/
22
+ *.egg
23
+
24
+ # uv
25
+ .uv/
26
+
27
+ # Environment variables
28
+ .env
29
+ .env.*
30
+
31
+ # IDEs
32
+ .vscode/
33
+ .idea/
34
+ *.iml
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Testing
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
44
+
45
+ # Mypy
46
+ .mypy_cache/
47
+
48
+ # Ruff
49
+ .ruff_cache/
50
+
51
+ # Spawn metadata
52
+ .spawn/
53
+
54
+ # Logs
55
+ logs/*.log
56
+
57
+ # ChromaDB vector store (regenerate with: delete chroma_db/ and re-run)
58
+ chroma_db/
59
+ !chroma_db/.gitkeep
60
+ """
61
+
62
+ AGENTS_MD_CONTENT = """# Agent Context: {project_name}
63
+
64
+ This file orients coding agents (Claude Code, and others that read
65
+ `AGENTS.md`) working in this repository.
66
+
67
+ ## Project structure
68
+
69
+ See the folder layout in `README.md`. This project was generated
70
+ with Spawn.
71
+
72
+ ## Setup
73
+
74
+ ```bash
75
+ uv sync
76
+ ```
77
+
78
+ ## Running tests
79
+
80
+ ```bash
81
+ uv run pytest
82
+ ```
83
+
84
+ ## Conventions
85
+
86
+ - Dependencies are managed with uv, not pip directly.
87
+ - Run `uv run ruff check .` before committing, if Ruff is configured
88
+ for this project (see `pyproject.toml`).
89
+ """
File without changes
spawn/utils/banner.py ADDED
@@ -0,0 +1,41 @@
1
+ from rich.console import Group
2
+ from rich.panel import Panel
3
+ from rich.text import Text
4
+
5
+ from spawn.utils.console import console
6
+
7
+
8
+ LINES = [
9
+ " ███████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗",
10
+ " ██╔════╝██╔══██╗██╔══██╗██║ ██║████╗ ██║",
11
+ " ███████╗██████╔╝███████║██║ █╗ ██║██╔██╗██║",
12
+ " ╚════██║██╔═══╝ ██╔══██║██║███╗██║██║╚████║",
13
+ " ███████║██║ ██║ ██║╚███╔███╔╝██║ ╚███║",
14
+ " ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚══╝",
15
+ ]
16
+
17
+ COLORS = ["#B9D6F2", "#94B8D9", "#6F9AC0", "#4A7CA7", "#2D5E8E", "#0F2D52"]
18
+
19
+
20
+ def show_banner() -> None:
21
+ logo_lines = []
22
+ for line_text, color in zip(LINES, COLORS):
23
+ t = Text(line_text)
24
+ t.stylize(f"bold {color}")
25
+ logo_lines.append(t)
26
+
27
+ subtitle = Text(" ✿ scaffold your next project", style="dim #94B8D9")
28
+ logo_group = Group(*logo_lines, subtitle)
29
+
30
+ console.print()
31
+ console.print(
32
+ Panel(
33
+ logo_group,
34
+ border_style="#2D5E8E",
35
+ padding=(1, 4),
36
+ expand=False,
37
+ title="[#94B8D9]◆[/#94B8D9]",
38
+ title_align="center",
39
+ )
40
+ )
41
+ console.print()
spawn/utils/console.py ADDED
@@ -0,0 +1,3 @@
1
+ from rich.console import Console
2
+
3
+ console = Console()