generate-tech-stack-mcp 0.2.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.
- generate_tech_stack_mcp/__init__.py +3 -0
- generate_tech_stack_mcp/analyze.py +1090 -0
- generate_tech_stack_mcp/server.py +163 -0
- generate_tech_stack_mcp-0.2.0.dist-info/METADATA +207 -0
- generate_tech_stack_mcp-0.2.0.dist-info/RECORD +9 -0
- generate_tech_stack_mcp-0.2.0.dist-info/WHEEL +5 -0
- generate_tech_stack_mcp-0.2.0.dist-info/entry_points.txt +3 -0
- generate_tech_stack_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
- generate_tech_stack_mcp-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
generate-tech-stack — project analyzer
|
|
4
|
+
Scans a project directory, detects all tools/libraries, and writes TECH_STACK.html.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python3 analyze.py [project_dir] [output_file]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Category metadata ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
CATEGORY_META = {
|
|
20
|
+
"language": {"icon": "🐍", "label": "Language & Runtime", "color": "green"},
|
|
21
|
+
"web": {"icon": "🌐", "label": "Web / API Framework", "color": "purple"},
|
|
22
|
+
"database": {"icon": "🗄️", "label": "Database / Storage", "color": "green"},
|
|
23
|
+
"ai_sdk": {"icon": "🧠", "label": "AI Guardrail SDKs", "color": "blue"},
|
|
24
|
+
"nlp": {"icon": "📝", "label": "NLP / ML", "color": "teal"},
|
|
25
|
+
"testing": {"icon": "🧪", "label": "Testing", "color": "yellow"},
|
|
26
|
+
"observability": {"icon": "📊", "label": "Observability", "color": "teal"},
|
|
27
|
+
"security": {"icon": "🔐", "label": "Security / Auth", "color": "rose"},
|
|
28
|
+
"infra": {"icon": "🚀", "label": "Infrastructure / Deploy", "color": "orange"},
|
|
29
|
+
"frontend": {"icon": "🖥️", "label": "Frontend / Dashboard", "color": "gray"},
|
|
30
|
+
"messaging": {"icon": "📬", "label": "Messaging / Comms", "color": "blue"},
|
|
31
|
+
"devtools": {"icon": "🛠️", "label": "Dev Tools", "color": "gray"},
|
|
32
|
+
"other": {"icon": "📦", "label": "Other Libraries", "color": "gray"},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# icon_bg, dot_color, title_color
|
|
36
|
+
COLOR_CSS = {
|
|
37
|
+
"green": ("#14532d", "#16a34a", "#4ade80"),
|
|
38
|
+
"purple": ("#2e1065", "#7c3aed", "#a78bfa"),
|
|
39
|
+
"blue": ("#1e3a5f", "#2563eb", "#60a5fa"),
|
|
40
|
+
"yellow": ("#422006", "#d97706", "#fbbf24"),
|
|
41
|
+
"teal": ("#134e4a", "#0d9488", "#2dd4bf"),
|
|
42
|
+
"rose": ("#4c0519", "#e11d48", "#fb7185"),
|
|
43
|
+
"orange": ("#431407", "#ea580c", "#fb923c"),
|
|
44
|
+
"gray": ("#1e293b", "#6b7280", "#9ca3af"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# badge bg, badge fg
|
|
48
|
+
BADGE_CSS = {
|
|
49
|
+
"pip": ("#1e293b", "#94a3b8"),
|
|
50
|
+
"dep": ("#14532d", "#86efac"),
|
|
51
|
+
"devDep": ("#1e1b4b", "#a5b4fc"),
|
|
52
|
+
"optional": ("#1e293b", "#64748b"),
|
|
53
|
+
"core": ("#14532d", "#86efac"),
|
|
54
|
+
"lang": ("#431407", "#fdba74"),
|
|
55
|
+
"deploy": ("#3b1f0f", "#fdba74"),
|
|
56
|
+
"prod": ("#1e293b", "#64748b"),
|
|
57
|
+
"ci": ("#1e1b4b", "#a5b4fc"),
|
|
58
|
+
"tool": ("#1e293b", "#94a3b8"),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Proper display names for pip package names
|
|
62
|
+
DISPLAY_NAMES = {
|
|
63
|
+
"fastapi": "FastAPI",
|
|
64
|
+
"flask": "Flask",
|
|
65
|
+
"django": "Django",
|
|
66
|
+
"starlette": "Starlette",
|
|
67
|
+
"uvicorn": "Uvicorn",
|
|
68
|
+
"gunicorn": "Gunicorn",
|
|
69
|
+
"httpx": "httpx",
|
|
70
|
+
"aiohttp": "aiohttp",
|
|
71
|
+
"requests": "Requests",
|
|
72
|
+
"pydantic": "Pydantic",
|
|
73
|
+
"aiofiles": "aiofiles",
|
|
74
|
+
"tornado": "Tornado",
|
|
75
|
+
"sanic": "Sanic",
|
|
76
|
+
"sqlalchemy": "SQLAlchemy",
|
|
77
|
+
"alembic": "Alembic",
|
|
78
|
+
"psycopg2": "psycopg2",
|
|
79
|
+
"psycopg2-binary": "PostgreSQL (psycopg2)",
|
|
80
|
+
"psycopg": "psycopg3",
|
|
81
|
+
"pymysql": "PyMySQL",
|
|
82
|
+
"motor": "Motor",
|
|
83
|
+
"pymongo": "PyMongo",
|
|
84
|
+
"redis": "Redis",
|
|
85
|
+
"aioredis": "aioredis",
|
|
86
|
+
"elasticsearch": "Elasticsearch",
|
|
87
|
+
"tortoise-orm": "Tortoise ORM",
|
|
88
|
+
"peewee": "Peewee",
|
|
89
|
+
"databases": "databases",
|
|
90
|
+
"asyncpg": "asyncpg",
|
|
91
|
+
"guardrails-ai": "GuardrailsAI",
|
|
92
|
+
"guardrails_ai": "GuardrailsAI",
|
|
93
|
+
"nemoguardrails": "NVIDIA NeMo",
|
|
94
|
+
"presidio-analyzer": "Presidio Analyzer",
|
|
95
|
+
"presidio-anonymizer": "Presidio Anonymizer",
|
|
96
|
+
"openai": "OpenAI",
|
|
97
|
+
"anthropic": "Anthropic",
|
|
98
|
+
"transformers": "Transformers",
|
|
99
|
+
"torch": "PyTorch",
|
|
100
|
+
"tensorflow": "TensorFlow",
|
|
101
|
+
"keras": "Keras",
|
|
102
|
+
"langchain": "LangChain",
|
|
103
|
+
"langchain-core": "LangChain Core",
|
|
104
|
+
"langchain-community": "LangChain Community",
|
|
105
|
+
"llama-index": "LlamaIndex",
|
|
106
|
+
"llama_index": "LlamaIndex",
|
|
107
|
+
"spacy": "spaCy",
|
|
108
|
+
"nltk": "NLTK",
|
|
109
|
+
"sentence-transformers": "Sentence Transformers",
|
|
110
|
+
"scikit-learn": "scikit-learn",
|
|
111
|
+
"numpy": "NumPy",
|
|
112
|
+
"pandas": "Pandas",
|
|
113
|
+
"cohere": "Cohere",
|
|
114
|
+
"google-generativeai": "Google Gemini",
|
|
115
|
+
"mistralai": "Mistral AI",
|
|
116
|
+
"tiktoken": "tiktoken",
|
|
117
|
+
"chromadb": "ChromaDB",
|
|
118
|
+
"pinecone-client": "Pinecone",
|
|
119
|
+
"weaviate-client": "Weaviate",
|
|
120
|
+
"qdrant-client": "Qdrant",
|
|
121
|
+
"faiss-cpu": "FAISS",
|
|
122
|
+
"pytest": "pytest",
|
|
123
|
+
"pytest-asyncio": "pytest-asyncio",
|
|
124
|
+
"pytest-cov": "pytest-cov",
|
|
125
|
+
"pytest-mock": "pytest-mock",
|
|
126
|
+
"hypothesis": "Hypothesis",
|
|
127
|
+
"factory-boy": "factory-boy",
|
|
128
|
+
"faker": "Faker",
|
|
129
|
+
"locust": "Locust",
|
|
130
|
+
"coverage": "Coverage",
|
|
131
|
+
"prometheus-client": "Prometheus Client",
|
|
132
|
+
"python-json-logger": "python-json-logger",
|
|
133
|
+
"opentelemetry-api": "OpenTelemetry API",
|
|
134
|
+
"opentelemetry-sdk": "OpenTelemetry SDK",
|
|
135
|
+
"sentry-sdk": "Sentry",
|
|
136
|
+
"datadog": "Datadog",
|
|
137
|
+
"elastic-apm": "Elastic APM",
|
|
138
|
+
"loguru": "Loguru",
|
|
139
|
+
"cryptography": "Cryptography",
|
|
140
|
+
"pyjwt": "PyJWT",
|
|
141
|
+
"passlib": "Passlib",
|
|
142
|
+
"python-jose": "python-jose",
|
|
143
|
+
"bcrypt": "bcrypt",
|
|
144
|
+
"paramiko": "Paramiko",
|
|
145
|
+
"authlib": "Authlib",
|
|
146
|
+
"celery": "Celery",
|
|
147
|
+
"dramatiq": "Dramatiq",
|
|
148
|
+
"boto3": "Boto3 (AWS)",
|
|
149
|
+
"google-cloud-storage": "GCS",
|
|
150
|
+
"azure-storage-blob": "Azure Blob",
|
|
151
|
+
"kubernetes": "Kubernetes",
|
|
152
|
+
"pika": "Pika (RabbitMQ)",
|
|
153
|
+
"kafka-python": "kafka-python",
|
|
154
|
+
"aiokafka": "aiokafka",
|
|
155
|
+
"pyyaml": "PyYAML",
|
|
156
|
+
"toml": "TOML",
|
|
157
|
+
"click": "Click",
|
|
158
|
+
"typer": "Typer",
|
|
159
|
+
"rich": "Rich",
|
|
160
|
+
"jinja2": "Jinja2",
|
|
161
|
+
"python-dotenv": "python-dotenv",
|
|
162
|
+
"pillow": "Pillow",
|
|
163
|
+
"websockets": "websockets",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Package maps ──────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
PYTHON_MAP = {
|
|
170
|
+
# Web
|
|
171
|
+
"fastapi": ("web", "REST API framework"),
|
|
172
|
+
"flask": ("web", "Micro web framework"),
|
|
173
|
+
"django": ("web", "Full-stack web framework"),
|
|
174
|
+
"starlette": ("web", "ASGI toolkit"),
|
|
175
|
+
"uvicorn": ("web", "ASGI server"),
|
|
176
|
+
"gunicorn": ("web", "WSGI HTTP server"),
|
|
177
|
+
"httpx": ("web", "Async HTTP client"),
|
|
178
|
+
"aiohttp": ("web", "Async HTTP client/server"),
|
|
179
|
+
"requests": ("web", "HTTP client"),
|
|
180
|
+
"pydantic": ("web", "Data validation"),
|
|
181
|
+
"aiofiles": ("web", "Async file I/O"),
|
|
182
|
+
"tornado": ("web", "Non-blocking web framework"),
|
|
183
|
+
"sanic": ("web", "Async web framework"),
|
|
184
|
+
# Database
|
|
185
|
+
"sqlalchemy": ("database", "ORM / query builder"),
|
|
186
|
+
"alembic": ("database", "DB migrations"),
|
|
187
|
+
"psycopg2": ("database", "PostgreSQL adapter"),
|
|
188
|
+
"psycopg2-binary": ("database", "PostgreSQL adapter"),
|
|
189
|
+
"psycopg": ("database", "PostgreSQL adapter v3"),
|
|
190
|
+
"pymysql": ("database", "MySQL adapter"),
|
|
191
|
+
"motor": ("database", "Async MongoDB driver"),
|
|
192
|
+
"pymongo": ("database", "MongoDB driver"),
|
|
193
|
+
"redis": ("database", "Redis client"),
|
|
194
|
+
"aioredis": ("database", "Async Redis client"),
|
|
195
|
+
"elasticsearch": ("database", "Elasticsearch client"),
|
|
196
|
+
"tortoise-orm": ("database", "Async ORM"),
|
|
197
|
+
"peewee": ("database", "Simple ORM"),
|
|
198
|
+
"databases": ("database", "Async DB queries"),
|
|
199
|
+
"asyncpg": ("database", "Async PostgreSQL driver"),
|
|
200
|
+
# AI / ML guardrail SDKs
|
|
201
|
+
"guardrails-ai": ("ai_sdk", "Composable AI validators"),
|
|
202
|
+
"guardrails_ai": ("ai_sdk", "Composable AI validators"),
|
|
203
|
+
"nemoguardrails": ("ai_sdk", "Colang state machine"),
|
|
204
|
+
"presidio-analyzer": ("ai_sdk", "PII detection"),
|
|
205
|
+
"presidio-anonymizer": ("nlp", "PII redaction engine"),
|
|
206
|
+
"openai": ("ai_sdk", "OpenAI API client"),
|
|
207
|
+
"anthropic": ("ai_sdk", "Anthropic API client"),
|
|
208
|
+
"transformers": ("ai_sdk", "Hugging Face Transformers"),
|
|
209
|
+
"torch": ("ai_sdk", "PyTorch"),
|
|
210
|
+
"tensorflow": ("ai_sdk", "TensorFlow"),
|
|
211
|
+
"keras": ("ai_sdk", "Deep learning API"),
|
|
212
|
+
"langchain": ("ai_sdk", "LLM orchestration"),
|
|
213
|
+
"langchain-core": ("ai_sdk", "LangChain core"),
|
|
214
|
+
"langchain-community": ("ai_sdk", "LangChain integrations"),
|
|
215
|
+
"llama-index": ("ai_sdk", "LlamaIndex RAG"),
|
|
216
|
+
"llama_index": ("ai_sdk", "LlamaIndex RAG"),
|
|
217
|
+
"spacy": ("nlp", "NLP pipeline"),
|
|
218
|
+
"nltk": ("nlp", "Natural language toolkit"),
|
|
219
|
+
"sentence-transformers": ("nlp", "Sentence embeddings"),
|
|
220
|
+
"scikit-learn": ("ai_sdk", "Machine learning"),
|
|
221
|
+
"numpy": ("ai_sdk", "Numerical computing"),
|
|
222
|
+
"pandas": ("ai_sdk", "Data manipulation"),
|
|
223
|
+
"cohere": ("ai_sdk", "Cohere API client"),
|
|
224
|
+
"google-generativeai": ("ai_sdk", "Google Gemini client"),
|
|
225
|
+
"mistralai": ("ai_sdk", "Mistral API client"),
|
|
226
|
+
"tiktoken": ("ai_sdk", "OpenAI tokenizer"),
|
|
227
|
+
"chromadb": ("database", "Vector database"),
|
|
228
|
+
"pinecone-client": ("database", "Pinecone vector DB"),
|
|
229
|
+
"weaviate-client": ("database", "Weaviate vector DB"),
|
|
230
|
+
"qdrant-client": ("database", "Qdrant vector DB"),
|
|
231
|
+
"faiss-cpu": ("ai_sdk", "FAISS similarity search"),
|
|
232
|
+
# Testing
|
|
233
|
+
"pytest": ("testing", "Test runner"),
|
|
234
|
+
"pytest-asyncio": ("testing", "Async test support"),
|
|
235
|
+
"pytest-cov": ("testing", "Coverage reporting"),
|
|
236
|
+
"pytest-mock": ("testing", "Mock helpers"),
|
|
237
|
+
"hypothesis": ("testing", "Property-based testing"),
|
|
238
|
+
"factory-boy": ("testing", "Test fixtures"),
|
|
239
|
+
"faker": ("testing", "Fake data generator"),
|
|
240
|
+
"locust": ("testing", "Load testing"),
|
|
241
|
+
"coverage": ("testing", "Code coverage"),
|
|
242
|
+
# Observability
|
|
243
|
+
"prometheus-client": ("observability", "Prometheus metrics"),
|
|
244
|
+
"python-json-logger": ("observability", "Structured JSON logging"),
|
|
245
|
+
"opentelemetry-api": ("observability", "OpenTelemetry tracing"),
|
|
246
|
+
"opentelemetry-sdk": ("observability", "OpenTelemetry SDK"),
|
|
247
|
+
"sentry-sdk": ("observability", "Error tracking"),
|
|
248
|
+
"datadog": ("observability", "Datadog APM"),
|
|
249
|
+
"elastic-apm": ("observability", "Elastic APM"),
|
|
250
|
+
"loguru": ("observability", "Modern logging"),
|
|
251
|
+
# Security
|
|
252
|
+
"cryptography": ("security", "Cryptographic primitives"),
|
|
253
|
+
"pyjwt": ("security", "JSON Web Tokens"),
|
|
254
|
+
"passlib": ("security", "Password hashing"),
|
|
255
|
+
"python-jose": ("security", "JOSE / JWT"),
|
|
256
|
+
"bcrypt": ("security", "bcrypt hashing"),
|
|
257
|
+
"paramiko": ("security", "SSH client"),
|
|
258
|
+
"authlib": ("security", "OAuth / OIDC"),
|
|
259
|
+
# Infra / messaging
|
|
260
|
+
"celery": ("infra", "Distributed task queue"),
|
|
261
|
+
"dramatiq": ("infra", "Task queue"),
|
|
262
|
+
"boto3": ("infra", "AWS SDK"),
|
|
263
|
+
"google-cloud-storage": ("infra", "GCS client"),
|
|
264
|
+
"azure-storage-blob": ("infra", "Azure Blob Storage"),
|
|
265
|
+
"kubernetes": ("infra", "Kubernetes client"),
|
|
266
|
+
"pika": ("messaging", "RabbitMQ client"),
|
|
267
|
+
"kafka-python": ("messaging", "Kafka client"),
|
|
268
|
+
"aiokafka": ("messaging", "Async Kafka client"),
|
|
269
|
+
# Other
|
|
270
|
+
"pyyaml": ("other", "YAML parser"),
|
|
271
|
+
"toml": ("other", "TOML parser"),
|
|
272
|
+
"click": ("other", "CLI framework"),
|
|
273
|
+
"typer": ("other", "CLI framework"),
|
|
274
|
+
"rich": ("other", "Terminal formatting"),
|
|
275
|
+
"jinja2": ("other", "Template engine"),
|
|
276
|
+
"python-dotenv": ("other", "Env var loader"),
|
|
277
|
+
"pillow": ("other", "Image processing"),
|
|
278
|
+
"websockets": ("other", "WebSocket library"),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
NODE_MAP = {
|
|
282
|
+
"express": ("web", "Web framework"),
|
|
283
|
+
"fastify": ("web", "Fast web framework"),
|
|
284
|
+
"koa": ("web", "Middleware framework"),
|
|
285
|
+
"next": ("frontend", "React framework"),
|
|
286
|
+
"nuxt": ("frontend", "Vue framework"),
|
|
287
|
+
"remix": ("frontend", "Full-stack React"),
|
|
288
|
+
"axios": ("web", "HTTP client"),
|
|
289
|
+
"node-fetch": ("web", "Fetch API for Node"),
|
|
290
|
+
"react": ("frontend", "UI component library"),
|
|
291
|
+
"react-dom": ("frontend", "React DOM renderer"),
|
|
292
|
+
"react-scripts": ("frontend", "CRA build toolchain"),
|
|
293
|
+
"vue": ("frontend", "Progressive UI framework"),
|
|
294
|
+
"svelte": ("frontend", "Compiled UI framework"),
|
|
295
|
+
"vite": ("devtools", "Build tool"),
|
|
296
|
+
"webpack": ("devtools", "Module bundler"),
|
|
297
|
+
"tailwindcss": ("frontend", "Utility CSS framework"),
|
|
298
|
+
"recharts": ("frontend", "Chart components"),
|
|
299
|
+
"chart.js": ("frontend", "Canvas charts"),
|
|
300
|
+
"d3": ("frontend", "Data visualisation"),
|
|
301
|
+
"lucide-react": ("frontend", "Icon library"),
|
|
302
|
+
"framer-motion": ("frontend", "Animation library"),
|
|
303
|
+
"prisma": ("database", "Type-safe ORM"),
|
|
304
|
+
"mongoose": ("database", "MongoDB ODM"),
|
|
305
|
+
"pg": ("database", "PostgreSQL client"),
|
|
306
|
+
"mysql2": ("database", "MySQL client"),
|
|
307
|
+
"redis": ("database", "Redis client"),
|
|
308
|
+
"ioredis": ("database", "Redis client"),
|
|
309
|
+
"sequelize": ("database", "Multi-dialect ORM"),
|
|
310
|
+
"knex": ("database", "Query builder"),
|
|
311
|
+
"typeorm": ("database", "TypeScript ORM"),
|
|
312
|
+
"drizzle-orm": ("database", "TypeScript ORM"),
|
|
313
|
+
"jest": ("testing", "Test runner"),
|
|
314
|
+
"vitest": ("testing", "Vite-native test runner"),
|
|
315
|
+
"mocha": ("testing", "Test framework"),
|
|
316
|
+
"chai": ("testing", "Assertion library"),
|
|
317
|
+
"cypress": ("testing", "E2E testing"),
|
|
318
|
+
"playwright": ("testing", "Browser automation"),
|
|
319
|
+
"supertest": ("testing", "HTTP assertion"),
|
|
320
|
+
"openai": ("ai_sdk", "OpenAI API client"),
|
|
321
|
+
"@anthropic-ai/sdk": ("ai_sdk", "Anthropic API client"),
|
|
322
|
+
"langchain": ("ai_sdk", "LLM orchestration"),
|
|
323
|
+
"@langchain/core": ("ai_sdk", "LangChain core"),
|
|
324
|
+
"jsonwebtoken": ("security", "JSON Web Tokens"),
|
|
325
|
+
"bcrypt": ("security", "Password hashing"),
|
|
326
|
+
"passport": ("security", "Auth middleware"),
|
|
327
|
+
"helmet": ("security", "HTTP security headers"),
|
|
328
|
+
"express-rate-limit": ("security", "Rate limiting"),
|
|
329
|
+
"pino": ("observability", "Structured logger"),
|
|
330
|
+
"winston": ("observability", "Logging library"),
|
|
331
|
+
"morgan": ("observability", "HTTP request logger"),
|
|
332
|
+
"bull": ("infra", "Job queue"),
|
|
333
|
+
"bullmq": ("infra", "Job queue"),
|
|
334
|
+
"socket.io": ("messaging", "WebSocket server"),
|
|
335
|
+
"ws": ("messaging", "WebSocket library"),
|
|
336
|
+
"aws-sdk": ("infra", "AWS SDK"),
|
|
337
|
+
"@aws-sdk/client-s3": ("infra", "AWS S3 client"),
|
|
338
|
+
"dotenv": ("other", "Env var loader"),
|
|
339
|
+
"zod": ("other", "Schema validation"),
|
|
340
|
+
"typescript": ("devtools", "TypeScript compiler"),
|
|
341
|
+
"eslint": ("devtools", "Linter"),
|
|
342
|
+
"prettier": ("devtools", "Code formatter"),
|
|
343
|
+
"esbuild": ("devtools", "Fast bundler"),
|
|
344
|
+
"turbo": ("devtools", "Monorepo build tool"),
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Packages detected via importlib.find_spec() or import_module() in source files
|
|
348
|
+
FIND_SPEC_MAP = {
|
|
349
|
+
"guardrails": ("ai_sdk", "GuardrailsAI", "Composable AI validators"),
|
|
350
|
+
"nemoguardrails": ("ai_sdk", "NVIDIA NeMo", "Colang state machine"),
|
|
351
|
+
"presidio_analyzer": ("ai_sdk", "Presidio Analyzer", "PII detection"),
|
|
352
|
+
"presidio": ("ai_sdk", "Presidio Analyzer", "PII analysis"),
|
|
353
|
+
"spacy": ("nlp", "spaCy", "NLP pipeline"),
|
|
354
|
+
"transformers": ("ai_sdk", "Transformers", "Hugging Face Transformers"),
|
|
355
|
+
"torch": ("ai_sdk", "PyTorch", "Deep learning"),
|
|
356
|
+
"openai": ("ai_sdk", "OpenAI", "OpenAI API client"),
|
|
357
|
+
"anthropic": ("ai_sdk", "Anthropic", "Anthropic API client"),
|
|
358
|
+
"langchain": ("ai_sdk", "LangChain", "LLM orchestration"),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ── Scanners ──────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
# Directories that contain third-party or generated code, never the project's own
|
|
365
|
+
# stack. Scanning them would report every installed dependency's dependencies
|
|
366
|
+
# (e.g. a local venv or vendored site-packages would add dozens of false tools).
|
|
367
|
+
EXCLUDED_DIRS = frozenset({
|
|
368
|
+
".git", ".hg", ".svn",
|
|
369
|
+
"node_modules", "bower_components",
|
|
370
|
+
"site-packages", "dist-packages",
|
|
371
|
+
"venv", ".venv", "env", "virtualenv",
|
|
372
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".tox", ".nox",
|
|
373
|
+
"dist", "build", "target", ".next", ".nuxt", ".output",
|
|
374
|
+
"vendor", "third_party", ".terraform",
|
|
375
|
+
"htmlcov", "coverage", ".cache",
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _iter_files(root: Path, pattern: str):
|
|
380
|
+
"""rglob that skips vendored/venv/build directories."""
|
|
381
|
+
for p in root.rglob(pattern):
|
|
382
|
+
parents = p.relative_to(root).parts[:-1]
|
|
383
|
+
if any(d in EXCLUDED_DIRS or d.endswith(".egg-info") for d in parents):
|
|
384
|
+
continue
|
|
385
|
+
yield p
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _normalize(name: str) -> str:
|
|
389
|
+
return name.lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _add_tool(tools: dict, cat: str, name: str, desc: str, badge: str) -> None:
|
|
393
|
+
"""Add a tool to the category, skipping if a normalized-equivalent entry exists."""
|
|
394
|
+
tools.setdefault(cat, [])
|
|
395
|
+
norm = _normalize(name)
|
|
396
|
+
if any(_normalize(t["name"]) == norm for t in tools[cat]):
|
|
397
|
+
return
|
|
398
|
+
tools[cat].append({"name": name, "desc": desc, "badge": badge})
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def scan_python(root: Path) -> dict:
|
|
402
|
+
tools: dict = {}
|
|
403
|
+
# Root manifests + one level of subdirectories (monorepos: backend/, api/, …)
|
|
404
|
+
req_candidates = [
|
|
405
|
+
root / f for f in ["requirements.txt", "requirements-dev.txt", "requirements/base.txt"]
|
|
406
|
+
] + [p for p in root.glob("*/requirements.txt") if p.parent.name not in EXCLUDED_DIRS]
|
|
407
|
+
for fpath in req_candidates:
|
|
408
|
+
if not fpath.exists():
|
|
409
|
+
continue
|
|
410
|
+
for line in fpath.read_text().splitlines():
|
|
411
|
+
line = line.strip()
|
|
412
|
+
if not line:
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
if line.startswith("#"):
|
|
416
|
+
# Detect commented-out package lines: # package>=version
|
|
417
|
+
m = re.match(r'^#\s*([\w][\w\-\.]*)\s*[>=<!\[]', line)
|
|
418
|
+
if m:
|
|
419
|
+
pkg = m.group(1).lower()
|
|
420
|
+
if pkg in PYTHON_MAP:
|
|
421
|
+
cat, desc = PYTHON_MAP[pkg]
|
|
422
|
+
display = DISPLAY_NAMES.get(pkg, pkg)
|
|
423
|
+
_add_tool(tools, cat, display, desc, "optional")
|
|
424
|
+
# Detect PostgreSQL from psycopg2 mentioned in comments
|
|
425
|
+
if "psycopg2" in line.lower() and not any(
|
|
426
|
+
_normalize(t["name"]) == "postgresql" for t in tools.get("database", [])
|
|
427
|
+
):
|
|
428
|
+
_add_tool(tools, "database", "PostgreSQL", "Production database", "optional")
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
pkg = re.split(r"[>=<!;\[#]", line)[0].strip().lower()
|
|
432
|
+
if pkg in PYTHON_MAP:
|
|
433
|
+
cat, desc = PYTHON_MAP[pkg]
|
|
434
|
+
display = DISPLAY_NAMES.get(pkg, pkg)
|
|
435
|
+
_add_tool(tools, cat, display, desc, "pip")
|
|
436
|
+
|
|
437
|
+
pp_candidates = [root / "pyproject.toml"] + [
|
|
438
|
+
p for p in root.glob("*/pyproject.toml") if p.parent.name not in EXCLUDED_DIRS
|
|
439
|
+
]
|
|
440
|
+
for pp in pp_candidates:
|
|
441
|
+
if not pp.exists():
|
|
442
|
+
continue
|
|
443
|
+
# Capture the package name at the start of each quoted string so pinned
|
|
444
|
+
# entries like "spacy>=3.0.0" or "guardrails-ai[extra]>=0.5" also match.
|
|
445
|
+
for pkg in re.findall(r'["\']([A-Za-z0-9][A-Za-z0-9._-]*)', pp.read_text()):
|
|
446
|
+
pkg = pkg.lower()
|
|
447
|
+
if pkg in PYTHON_MAP:
|
|
448
|
+
cat, desc = PYTHON_MAP[pkg]
|
|
449
|
+
display = DISPLAY_NAMES.get(pkg, pkg)
|
|
450
|
+
_add_tool(tools, cat, display, desc, "pip")
|
|
451
|
+
return tools
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def scan_node(root: Path) -> dict:
|
|
455
|
+
tools: dict = {}
|
|
456
|
+
# Scan root package.json + one level of subdirectories (monorepos / frontend dirs)
|
|
457
|
+
pj_candidates = [root / "package.json"] + [
|
|
458
|
+
p for p in root.glob("*/package.json") if p.parent.name not in EXCLUDED_DIRS
|
|
459
|
+
]
|
|
460
|
+
for pj in pj_candidates:
|
|
461
|
+
if not pj.exists():
|
|
462
|
+
continue
|
|
463
|
+
try:
|
|
464
|
+
data = json.loads(pj.read_text())
|
|
465
|
+
except Exception:
|
|
466
|
+
continue
|
|
467
|
+
all_deps: dict = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
468
|
+
for pkg in all_deps:
|
|
469
|
+
key = pkg.lower()
|
|
470
|
+
if key in NODE_MAP:
|
|
471
|
+
cat, desc = NODE_MAP[key]
|
|
472
|
+
badge = "devDep" if pkg in data.get("devDependencies", {}) else "dep"
|
|
473
|
+
_add_tool(tools, cat, pkg, desc, badge)
|
|
474
|
+
return tools
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def scan_python_source(root: Path) -> dict:
|
|
478
|
+
"""Detect optional/dynamic SDKs by scanning Python source for find_spec() calls and backend enums."""
|
|
479
|
+
tools: dict = {}
|
|
480
|
+
|
|
481
|
+
find_spec_re = re.compile(r'find_spec\(["\'](\w+)["\']')
|
|
482
|
+
import_mod_re = re.compile(r'import_module\(["\'](\w+)["\']')
|
|
483
|
+
lakera_re = re.compile(r'lakera\.ai|["\']lakera["\']|LAKERA\s*=')
|
|
484
|
+
ga_guard_re = re.compile(r'GA_GUARD|["\']ga_guard["\']')
|
|
485
|
+
presidio_anon_re = re.compile(r'presidio_anonymizer|presidio-anonymizer')
|
|
486
|
+
|
|
487
|
+
seen_mods: set = set()
|
|
488
|
+
has_lakera = False
|
|
489
|
+
has_ga_guard = False
|
|
490
|
+
has_pres_anon = False
|
|
491
|
+
|
|
492
|
+
for py_file in _iter_files(root, "*.py"):
|
|
493
|
+
try:
|
|
494
|
+
content = py_file.read_text(errors="ignore")
|
|
495
|
+
except Exception:
|
|
496
|
+
continue
|
|
497
|
+
for m in find_spec_re.finditer(content):
|
|
498
|
+
seen_mods.add(m.group(1))
|
|
499
|
+
for m in import_mod_re.finditer(content):
|
|
500
|
+
seen_mods.add(m.group(1))
|
|
501
|
+
if lakera_re.search(content):
|
|
502
|
+
has_lakera = True
|
|
503
|
+
if ga_guard_re.search(content):
|
|
504
|
+
has_ga_guard = True
|
|
505
|
+
if presidio_anon_re.search(content):
|
|
506
|
+
has_pres_anon = True
|
|
507
|
+
|
|
508
|
+
for mod in seen_mods:
|
|
509
|
+
if mod in FIND_SPEC_MAP:
|
|
510
|
+
cat, name, desc = FIND_SPEC_MAP[mod]
|
|
511
|
+
_add_tool(tools, cat, name, desc, "optional")
|
|
512
|
+
|
|
513
|
+
if has_lakera:
|
|
514
|
+
_add_tool(tools, "ai_sdk", "Lakera Guard", "Cloud REST prompt-injection API", "optional")
|
|
515
|
+
if has_ga_guard:
|
|
516
|
+
_add_tool(tools, "ai_sdk", "GA Guard", "Adversarial content detection", "optional")
|
|
517
|
+
if has_pres_anon:
|
|
518
|
+
# Only add if not already captured from commented requirements in any category
|
|
519
|
+
already = any(
|
|
520
|
+
_normalize(t["name"]) == "presidioanonymizer"
|
|
521
|
+
for items in tools.values() for t in items
|
|
522
|
+
)
|
|
523
|
+
if not already:
|
|
524
|
+
_add_tool(tools, "nlp", "Presidio Anonymizer", "PII redaction engine", "optional")
|
|
525
|
+
|
|
526
|
+
return tools
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def detect_languages(root: Path) -> list:
|
|
530
|
+
langs = []
|
|
531
|
+
seen: set = set()
|
|
532
|
+
checks = [
|
|
533
|
+
("requirements.txt", "Python", "Primary language"),
|
|
534
|
+
("pyproject.toml", "Python", "Primary language"),
|
|
535
|
+
("package.json", "JavaScript / TypeScript", "Primary language"),
|
|
536
|
+
("go.mod", "Go", "Primary language"),
|
|
537
|
+
("Cargo.toml", "Rust", "Primary language"),
|
|
538
|
+
("pom.xml", "Java (Maven)", "Primary language"),
|
|
539
|
+
("build.gradle", "Java / Kotlin", "Primary language"),
|
|
540
|
+
("composer.json", "PHP", "Primary language"),
|
|
541
|
+
("Gemfile", "Ruby", "Primary language"),
|
|
542
|
+
]
|
|
543
|
+
for fname, lang, desc in checks:
|
|
544
|
+
if (root / fname).exists() and lang not in seen:
|
|
545
|
+
seen.add(lang)
|
|
546
|
+
langs.append({"name": lang, "desc": desc, "badge": "lang"})
|
|
547
|
+
if lang == "Python":
|
|
548
|
+
langs.append({"name": "pip / venv", "desc": "Package management", "badge": "lang"})
|
|
549
|
+
if (root / "tsconfig.json").exists() or list(root.glob("src/**/*.ts")):
|
|
550
|
+
if "TypeScript" not in seen:
|
|
551
|
+
langs.append({"name": "TypeScript", "desc": "Type-safe JS", "badge": "lang"})
|
|
552
|
+
return langs
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def detect_databases_from_files(root: Path) -> list:
|
|
556
|
+
"""Detect database systems from docker-compose, env files, and deployment configs."""
|
|
557
|
+
found: list = []
|
|
558
|
+
seen: set = set()
|
|
559
|
+
|
|
560
|
+
def add(name, desc, badge):
|
|
561
|
+
norm = _normalize(name)
|
|
562
|
+
if norm not in seen:
|
|
563
|
+
seen.add(norm)
|
|
564
|
+
found.append({"name": name, "desc": desc, "badge": badge})
|
|
565
|
+
|
|
566
|
+
config_files = (
|
|
567
|
+
["docker-compose.yml", "docker-compose.yaml", ".env", ".env.example", ".env.sample"]
|
|
568
|
+
+ [f.name for f in root.glob("*.env")]
|
|
569
|
+
)
|
|
570
|
+
for fname in config_files:
|
|
571
|
+
fpath = root / fname
|
|
572
|
+
if not fpath.exists():
|
|
573
|
+
continue
|
|
574
|
+
try:
|
|
575
|
+
content = fpath.read_text(errors="ignore").lower()
|
|
576
|
+
except Exception:
|
|
577
|
+
continue
|
|
578
|
+
if "sqlite://" in content:
|
|
579
|
+
add("SQLite", "Embedded / dev database", "core")
|
|
580
|
+
if any(x in content for x in ["postgres://", "postgresql://", "image: postgres", "image: postgresql"]):
|
|
581
|
+
add("PostgreSQL", "Production database", "prod")
|
|
582
|
+
if any(x in content for x in ["redis://", "image: redis"]):
|
|
583
|
+
add("Redis", "Cache / distributed rate limiting", "optional")
|
|
584
|
+
if "mongodb://" in content or "image: mongo" in content:
|
|
585
|
+
add("MongoDB", "Document store", "optional")
|
|
586
|
+
if "mysql://" in content or "image: mysql" in content:
|
|
587
|
+
add("MySQL", "Relational database", "optional")
|
|
588
|
+
|
|
589
|
+
return found
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def detect_infra(root: Path) -> list:
|
|
593
|
+
infra = []
|
|
594
|
+
if (root / "Dockerfile").exists():
|
|
595
|
+
infra.append({"name": "Docker", "desc": "Container image", "badge": "deploy"})
|
|
596
|
+
for name in ["docker-compose.yml", "docker-compose.yaml"]:
|
|
597
|
+
if (root / name).exists():
|
|
598
|
+
infra.append({"name": "Docker Compose", "desc": "Multi-service stack", "badge": "deploy"})
|
|
599
|
+
break
|
|
600
|
+
yamls = [f.name for f in _iter_files(root, "*.yaml")] + [f.name for f in _iter_files(root, "*.yml")]
|
|
601
|
+
if any("k8s" in y or "kubernetes" in y or "deployment" in y for y in yamls):
|
|
602
|
+
infra.append({"name": "Kubernetes", "desc": "Container orchestration", "badge": "prod"})
|
|
603
|
+
if (root / ".github" / "workflows").exists():
|
|
604
|
+
infra.append({"name": "GitHub Actions", "desc": "CI/CD pipeline", "badge": "ci"})
|
|
605
|
+
if (root / ".gitlab-ci.yml").exists():
|
|
606
|
+
infra.append({"name": "GitLab CI", "desc": "CI/CD pipeline", "badge": "ci"})
|
|
607
|
+
if (root / "nginx.conf").exists() or (root / "nginx").is_dir():
|
|
608
|
+
infra.append({"name": "nginx", "desc": "Reverse proxy / TLS", "badge": "prod"})
|
|
609
|
+
if (root / "Caddyfile").exists():
|
|
610
|
+
infra.append({"name": "Caddy", "desc": "Automatic HTTPS proxy", "badge": "prod"})
|
|
611
|
+
if (root / "alembic.ini").exists():
|
|
612
|
+
infra.append({"name": "Alembic Migrations", "desc": "DB schema version control", "badge": "tool"})
|
|
613
|
+
return infra
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def collect(root: Path) -> dict:
|
|
617
|
+
tools: dict = {}
|
|
618
|
+
|
|
619
|
+
langs = detect_languages(root)
|
|
620
|
+
if langs:
|
|
621
|
+
tools["language"] = langs
|
|
622
|
+
|
|
623
|
+
# Requirements / package.json
|
|
624
|
+
for cat, items in scan_python(root).items():
|
|
625
|
+
tools.setdefault(cat, []).extend(items)
|
|
626
|
+
for cat, items in scan_node(root).items():
|
|
627
|
+
tools.setdefault(cat, []).extend(items)
|
|
628
|
+
|
|
629
|
+
# Source-based detection (runs after requirements so it can fill gaps)
|
|
630
|
+
for cat, items in scan_python_source(root).items():
|
|
631
|
+
for item in items:
|
|
632
|
+
_add_tool(tools, cat, item["name"], item["desc"], item["badge"])
|
|
633
|
+
|
|
634
|
+
# Database systems from deployment config
|
|
635
|
+
db_tools = detect_databases_from_files(root)
|
|
636
|
+
for t in db_tools:
|
|
637
|
+
_add_tool(tools, "database", t["name"], t["desc"], t["badge"])
|
|
638
|
+
|
|
639
|
+
# Infra
|
|
640
|
+
infra = detect_infra(root)
|
|
641
|
+
if infra:
|
|
642
|
+
tools.setdefault("infra", []).extend(infra)
|
|
643
|
+
|
|
644
|
+
# Final dedup within each category by normalized name
|
|
645
|
+
for cat in tools:
|
|
646
|
+
seen: set = set()
|
|
647
|
+
uniq = []
|
|
648
|
+
for t in tools[cat]:
|
|
649
|
+
key = _normalize(t["name"])
|
|
650
|
+
if key not in seen:
|
|
651
|
+
seen.add(key)
|
|
652
|
+
uniq.append(t)
|
|
653
|
+
tools[cat] = uniq
|
|
654
|
+
|
|
655
|
+
return tools
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# ── Architecture diagram ──────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
def build_arch_html(tools: dict) -> str:
|
|
661
|
+
web_items = [t["name"] for t in tools.get("web", [])[:4]]
|
|
662
|
+
ai_items = [t["name"] for t in tools.get("ai_sdk", [])[:6]]
|
|
663
|
+
nlp_items = [t["name"] for t in tools.get("nlp", [])[:3]]
|
|
664
|
+
db_items = [t["name"] for t in tools.get("database", [])[:3]]
|
|
665
|
+
obs_items = [t["name"] for t in tools.get("observability", [])[:2]]
|
|
666
|
+
fe_items = [t["name"] for t in tools.get("frontend", [])[:2]]
|
|
667
|
+
has_sec = bool(tools.get("security"))
|
|
668
|
+
|
|
669
|
+
S = 'style="font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:#334155;text-align:center;padding:2px 0;margin-bottom:6px"'
|
|
670
|
+
|
|
671
|
+
def lbl(txt):
|
|
672
|
+
return f'<div {S}>{txt}</div>'
|
|
673
|
+
|
|
674
|
+
def arrow():
|
|
675
|
+
return '<div style="color:#1e3a5f;font-size:1.3rem;text-align:center;padding:4px 0;line-height:1">↓</div>'
|
|
676
|
+
|
|
677
|
+
rows = []
|
|
678
|
+
|
|
679
|
+
# Layer 1 — Consumer
|
|
680
|
+
app_label = "Your LLM Application" if (ai_items or nlp_items) else "Your Application"
|
|
681
|
+
app_sub = "Chatbot / Agent / RAG Pipeline" if (ai_items or nlp_items) else "Client / Consumer"
|
|
682
|
+
rows.append(lbl("CONSUMER"))
|
|
683
|
+
rows.append(
|
|
684
|
+
'<div class="arch-row">'
|
|
685
|
+
'<div class="arch-box" style="background:#1e1b4b;border-color:#4338ca;color:#a5b4fc;min-width:360px">'
|
|
686
|
+
f'{app_label}'
|
|
687
|
+
f'<div style="font-size:.65rem;opacity:.6;margin-top:4px;font-weight:400">{app_sub}</div>'
|
|
688
|
+
'</div></div>'
|
|
689
|
+
)
|
|
690
|
+
rows.append(arrow())
|
|
691
|
+
|
|
692
|
+
# Layer 2 — API / Middleware
|
|
693
|
+
if web_items:
|
|
694
|
+
label = " · ".join(web_items)
|
|
695
|
+
sub_parts: list = []
|
|
696
|
+
if has_sec:
|
|
697
|
+
sub_parts.append("API Key Auth")
|
|
698
|
+
sub_parts += ["Rate Limiting", "CORS Middleware"]
|
|
699
|
+
rows.append(lbl("API / MIDDLEWARE"))
|
|
700
|
+
rows.append(
|
|
701
|
+
'<div class="arch-row">'
|
|
702
|
+
'<div class="arch-box" style="background:#14532d;border-color:#16a34a;color:#86efac;min-width:360px">'
|
|
703
|
+
f'{label}'
|
|
704
|
+
f'<div style="font-size:.65rem;opacity:.6;margin-top:4px;font-weight:400">'
|
|
705
|
+
f'{" · ".join(sub_parts)}</div>'
|
|
706
|
+
'</div></div>'
|
|
707
|
+
)
|
|
708
|
+
rows.append(arrow())
|
|
709
|
+
|
|
710
|
+
# Layer 3 — AI Guardrails + NLP
|
|
711
|
+
all_ai = ai_items + nlp_items
|
|
712
|
+
if all_ai:
|
|
713
|
+
chips = "".join(
|
|
714
|
+
f'<span style="background:#162744;border:1px solid rgba(37,99,235,.4);border-radius:6px;'
|
|
715
|
+
f'padding:4px 11px;font-size:.72rem;font-weight:600;color:#93c5fd">{n}</span>'
|
|
716
|
+
for n in all_ai
|
|
717
|
+
)
|
|
718
|
+
rows.append(lbl("AI GUARDRAILS & NLP"))
|
|
719
|
+
rows.append(
|
|
720
|
+
'<div class="arch-row">'
|
|
721
|
+
'<div class="arch-box" style="background:#1a2d4a;border-color:#2563eb;color:#60a5fa;'
|
|
722
|
+
'min-width:360px;max-width:560px;width:100%">'
|
|
723
|
+
f'<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:8px">{chips}</div>'
|
|
724
|
+
'<div style="font-size:.6rem;color:#475569">'
|
|
725
|
+
'Optional — loaded at runtime via importlib.find_spec()</div>'
|
|
726
|
+
'</div></div>'
|
|
727
|
+
)
|
|
728
|
+
rows.append(arrow())
|
|
729
|
+
|
|
730
|
+
# Layer 4 — Data · Observability · Frontend
|
|
731
|
+
bottom: list = []
|
|
732
|
+
if db_items:
|
|
733
|
+
inner = "".join(
|
|
734
|
+
f'<div style="font-size:.72rem;font-weight:500;padding:2px 0">{n}</div>'
|
|
735
|
+
for n in db_items
|
|
736
|
+
)
|
|
737
|
+
bottom.append(
|
|
738
|
+
'<div class="arch-box" style="background:#1c1917;border-color:#78350f;'
|
|
739
|
+
'color:#fbbf24;flex:1;min-width:130px">'
|
|
740
|
+
'<div style="font-size:.58rem;text-transform:uppercase;letter-spacing:.1em;'
|
|
741
|
+
'opacity:.5;margin-bottom:6px">Storage</div>'
|
|
742
|
+
+ inner + '</div>'
|
|
743
|
+
)
|
|
744
|
+
if obs_items:
|
|
745
|
+
inner = "".join(
|
|
746
|
+
f'<div style="font-size:.72rem;font-weight:500;padding:2px 0">{n}</div>'
|
|
747
|
+
for n in obs_items
|
|
748
|
+
)
|
|
749
|
+
bottom.append(
|
|
750
|
+
'<div class="arch-box" style="background:#134e4a;border-color:#0d9488;'
|
|
751
|
+
'color:#5eead4;flex:1;min-width:130px">'
|
|
752
|
+
'<div style="font-size:.58rem;text-transform:uppercase;letter-spacing:.1em;'
|
|
753
|
+
'opacity:.5;margin-bottom:6px">Observability</div>'
|
|
754
|
+
+ inner + '</div>'
|
|
755
|
+
)
|
|
756
|
+
if fe_items:
|
|
757
|
+
inner = "".join(
|
|
758
|
+
f'<div style="font-size:.72rem;font-weight:500;padding:2px 0">{n}</div>'
|
|
759
|
+
for n in fe_items
|
|
760
|
+
)
|
|
761
|
+
bottom.append(
|
|
762
|
+
'<div class="arch-box" style="background:#1e293b;border-color:#475569;'
|
|
763
|
+
'color:#94a3b8;flex:1;min-width:130px">'
|
|
764
|
+
'<div style="font-size:.58rem;text-transform:uppercase;letter-spacing:.1em;'
|
|
765
|
+
'opacity:.5;margin-bottom:6px">Dashboard</div>'
|
|
766
|
+
+ inner + '</div>'
|
|
767
|
+
)
|
|
768
|
+
if bottom:
|
|
769
|
+
rows.append(lbl("DATA · OBSERVABILITY · FRONTEND"))
|
|
770
|
+
rows.append(
|
|
771
|
+
f'<div class="arch-row" style="align-items:stretch;gap:12px">{"".join(bottom)}</div>'
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
return "\n ".join(rows)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ── HTML renderer ─────────────────────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
def render_html(tools: dict, project_name: str) -> str:
|
|
780
|
+
total = sum(len(v) for v in tools.values())
|
|
781
|
+
cat_count = len(tools)
|
|
782
|
+
max_count = max((len(v) for v in tools.values()), default=1)
|
|
783
|
+
ai_count = len(tools.get("ai_sdk", []))
|
|
784
|
+
db_count = len(tools.get("database", []))
|
|
785
|
+
today = datetime.date.today().strftime("%B %d, %Y")
|
|
786
|
+
|
|
787
|
+
arch_html = build_arch_html(tools)
|
|
788
|
+
|
|
789
|
+
# ── Bar chart ──
|
|
790
|
+
bars_html = ""
|
|
791
|
+
for cat, items in tools.items():
|
|
792
|
+
if not items:
|
|
793
|
+
continue
|
|
794
|
+
meta = CATEGORY_META.get(cat, CATEGORY_META["other"])
|
|
795
|
+
_, dot_color, _ = COLOR_CSS.get(meta["color"], COLOR_CSS["gray"])
|
|
796
|
+
pct = round(len(items) / max_count * 100)
|
|
797
|
+
bars_html += (
|
|
798
|
+
'<div class="bar-row">'
|
|
799
|
+
f'<div class="bar-lbl"><span class="bar-icon">{meta["icon"]}</span>{meta["label"]}</div>'
|
|
800
|
+
f'<div class="bar-outer"><div class="bar-inner" style="width:{pct}%;background:{dot_color}"></div></div>'
|
|
801
|
+
f'<span class="bar-num" style="color:{dot_color}">{len(items)}</span>'
|
|
802
|
+
'</div>'
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# ── Cards ──
|
|
806
|
+
cards_html = ""
|
|
807
|
+
for cat, items in tools.items():
|
|
808
|
+
if not items:
|
|
809
|
+
continue
|
|
810
|
+
meta = CATEGORY_META.get(cat, CATEGORY_META["other"])
|
|
811
|
+
icon_bg, dot_color, title_color = COLOR_CSS.get(meta["color"], COLOR_CSS["gray"])
|
|
812
|
+
plural = "tools" if len(items) != 1 else "tool"
|
|
813
|
+
|
|
814
|
+
tool_rows = ""
|
|
815
|
+
for t in items:
|
|
816
|
+
bg, fg = BADGE_CSS.get(t.get("badge", "tool"), BADGE_CSS["tool"])
|
|
817
|
+
tool_rows += (
|
|
818
|
+
'<div class="tool">'
|
|
819
|
+
f'<span class="dot" style="background:{dot_color}"></span>'
|
|
820
|
+
f'<span class="tool-name">{t["name"]}</span>'
|
|
821
|
+
f'<span class="tool-desc">{t["desc"]}</span>'
|
|
822
|
+
f'<span class="badge" style="background:{bg};color:{fg}">{t.get("badge", "")}</span>'
|
|
823
|
+
'</div>'
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
cards_html += (
|
|
827
|
+
f'<div class="card" style="--c:{dot_color};--c-bg:{icon_bg}">'
|
|
828
|
+
'<div class="card-top">'
|
|
829
|
+
f'<span class="card-icon" style="background:{icon_bg}">{meta["icon"]}</span>'
|
|
830
|
+
'<div>'
|
|
831
|
+
f'<div class="card-title" style="color:{title_color}">{meta["label"]}</div>'
|
|
832
|
+
f'<div class="card-sub">{len(items)} {plural}</div>'
|
|
833
|
+
'</div>'
|
|
834
|
+
'</div>'
|
|
835
|
+
f'<div class="tool-list">{tool_rows}</div>'
|
|
836
|
+
'</div>'
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# ── Stat cards ──
|
|
840
|
+
stats_html = (
|
|
841
|
+
'<div class="stat-card">'
|
|
842
|
+
f'<div class="stat-num">{total}</div>'
|
|
843
|
+
'<div class="stat-lbl">Total Tools</div>'
|
|
844
|
+
'</div>'
|
|
845
|
+
'<div class="stat-card">'
|
|
846
|
+
f'<div class="stat-num">{cat_count}</div>'
|
|
847
|
+
'<div class="stat-lbl">Categories</div>'
|
|
848
|
+
'</div>'
|
|
849
|
+
)
|
|
850
|
+
if ai_count:
|
|
851
|
+
stats_html += (
|
|
852
|
+
'<div class="stat-card stat-accent">'
|
|
853
|
+
f'<div class="stat-num">{ai_count}</div>'
|
|
854
|
+
'<div class="stat-lbl">AI Guardrails</div>'
|
|
855
|
+
'</div>'
|
|
856
|
+
)
|
|
857
|
+
if db_count:
|
|
858
|
+
stats_html += (
|
|
859
|
+
'<div class="stat-card">'
|
|
860
|
+
f'<div class="stat-num">{db_count}</div>'
|
|
861
|
+
'<div class="stat-lbl">Data Stores</div>'
|
|
862
|
+
'</div>'
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# CSS as a plain string (no f-string) so CSS braces don't need escaping
|
|
866
|
+
css = (
|
|
867
|
+
"*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}"
|
|
868
|
+
":root{"
|
|
869
|
+
"--bg:#0f172a;--bg2:#111827;--bg3:#161e2e;--bg4:#1a2535;"
|
|
870
|
+
"--border:#1e293b;--border2:#263347;"
|
|
871
|
+
"--text:#f1f5f9;--muted:#64748b;--dim:#334155;"
|
|
872
|
+
"--green:#22c55e;"
|
|
873
|
+
"--mono:'JetBrains Mono',monospace;"
|
|
874
|
+
"--sans:'IBM Plex Sans',system-ui,sans-serif;"
|
|
875
|
+
"}"
|
|
876
|
+
"html{scroll-behavior:smooth}"
|
|
877
|
+
"body{font-family:var(--sans);background:var(--bg);color:var(--text);"
|
|
878
|
+
"line-height:1.6;-webkit-font-smoothing:antialiased}"
|
|
879
|
+
".header{"
|
|
880
|
+
"padding:80px 24px 56px;text-align:center;position:relative;overflow:hidden;"
|
|
881
|
+
"border-bottom:1px solid var(--border);}"
|
|
882
|
+
".header::before{"
|
|
883
|
+
"content:'';position:absolute;inset:0;"
|
|
884
|
+
"background:radial-gradient(ellipse 90% 60% at 50% -10%,"
|
|
885
|
+
"rgba(34,197,94,.07) 0%,transparent 70%);pointer-events:none;}"
|
|
886
|
+
".header-badge{"
|
|
887
|
+
"display:inline-flex;align-items:center;gap:7px;"
|
|
888
|
+
"background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);"
|
|
889
|
+
"border-radius:999px;padding:5px 14px;margin-bottom:24px;"
|
|
890
|
+
"font-size:.7rem;font-weight:600;letter-spacing:.1em;"
|
|
891
|
+
"text-transform:uppercase;color:#22c55e;}"
|
|
892
|
+
".badge-dot{"
|
|
893
|
+
"width:6px;height:6px;background:#22c55e;border-radius:50%;"
|
|
894
|
+
"animation:pulse 2s ease-in-out infinite;}"
|
|
895
|
+
"@keyframes pulse{"
|
|
896
|
+
"0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}"
|
|
897
|
+
"50%{box-shadow:0 0 0 5px rgba(34,197,94,0)}}"
|
|
898
|
+
"h1{"
|
|
899
|
+
"font-size:clamp(1.8rem,4.5vw,3rem);font-weight:700;letter-spacing:-.03em;"
|
|
900
|
+
"color:#f8fafc;margin-bottom:10px;line-height:1.15;}"
|
|
901
|
+
".header-sub{color:var(--muted);font-size:.9rem;font-weight:400}"
|
|
902
|
+
".stats-wrap{"
|
|
903
|
+
"display:flex;flex-wrap:wrap;justify-content:center;gap:14px;"
|
|
904
|
+
"max-width:820px;margin:0 auto;padding:40px 24px 60px;}"
|
|
905
|
+
".stat-card{"
|
|
906
|
+
"flex:1;min-width:140px;max-width:190px;"
|
|
907
|
+
"background:var(--bg3);border:1px solid var(--border);"
|
|
908
|
+
"border-radius:14px;padding:20px 16px;text-align:center;"
|
|
909
|
+
"transition:border-color .2s,transform .2s;}"
|
|
910
|
+
".stat-card:hover{border-color:var(--border2);transform:translateY(-2px)}"
|
|
911
|
+
".stat-card.stat-accent{background:rgba(34,197,94,.05);border-color:rgba(34,197,94,.25)}"
|
|
912
|
+
".stat-card.stat-accent .stat-num{color:#22c55e}"
|
|
913
|
+
".stat-num{font-family:var(--mono);font-size:2.4rem;font-weight:600;"
|
|
914
|
+
"color:#f8fafc;line-height:1}"
|
|
915
|
+
".stat-lbl{font-size:.65rem;color:var(--muted);text-transform:uppercase;"
|
|
916
|
+
"letter-spacing:.1em;margin-top:6px}"
|
|
917
|
+
".section{max-width:1200px;margin:0 auto;padding:0 24px 72px}"
|
|
918
|
+
".sec-hdr{display:flex;align-items:center;gap:14px;margin-bottom:32px}"
|
|
919
|
+
".sec-title{font-size:.65rem;font-weight:700;text-transform:uppercase;"
|
|
920
|
+
"letter-spacing:.15em;color:var(--muted);white-space:nowrap}"
|
|
921
|
+
".sec-line{flex:1;height:1px;background:var(--border)}"
|
|
922
|
+
".arch{display:flex;flex-direction:column;align-items:center;gap:0;"
|
|
923
|
+
"max-width:900px;margin:0 auto}"
|
|
924
|
+
".arch-row{display:flex;align-items:center;justify-content:center;"
|
|
925
|
+
"gap:10px;flex-wrap:wrap}"
|
|
926
|
+
".arch-box{"
|
|
927
|
+
"border-radius:10px;padding:13px 20px;text-align:center;font-size:.82rem;"
|
|
928
|
+
"font-weight:600;border:1px solid;min-width:120px;"
|
|
929
|
+
"transition:transform .2s,box-shadow .2s;}"
|
|
930
|
+
".arch-box:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.4)}"
|
|
931
|
+
".chart-wrap{"
|
|
932
|
+
"background:var(--bg3);border:1px solid var(--border);"
|
|
933
|
+
"border-radius:16px;padding:28px 28px 20px;max-width:720px;margin:0 auto;}"
|
|
934
|
+
".bar-row{display:flex;align-items:center;gap:14px;margin-bottom:13px}"
|
|
935
|
+
".bar-row:last-child{margin-bottom:0}"
|
|
936
|
+
".bar-lbl{display:flex;align-items:center;gap:8px;font-size:.78rem;"
|
|
937
|
+
"color:#94a3b8;width:225px;flex-shrink:0;white-space:nowrap;"
|
|
938
|
+
"overflow:hidden;text-overflow:ellipsis;}"
|
|
939
|
+
".bar-icon{font-size:.9rem}"
|
|
940
|
+
".bar-outer{flex:1;height:10px;background:#0a0f1a;border-radius:999px;overflow:hidden}"
|
|
941
|
+
".bar-inner{height:10px;border-radius:999px;transition:width .6s ease}"
|
|
942
|
+
".bar-num{font-family:var(--mono);font-size:.75rem;font-weight:600;"
|
|
943
|
+
"width:20px;text-align:right;flex-shrink:0}"
|
|
944
|
+
".cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px}"
|
|
945
|
+
".card{"
|
|
946
|
+
"background:var(--bg3);border:1px solid var(--border);"
|
|
947
|
+
"border-left:3px solid var(--c,#475569);border-radius:14px;"
|
|
948
|
+
"overflow:hidden;transition:border-color .2s,transform .2s,box-shadow .2s;}"
|
|
949
|
+
".card:hover{"
|
|
950
|
+
"border-color:var(--c,#475569);transform:translateY(-3px);"
|
|
951
|
+
"box-shadow:0 8px 28px rgba(0,0,0,.35);}"
|
|
952
|
+
".card-top{"
|
|
953
|
+
"display:flex;align-items:center;gap:12px;padding:18px 20px 14px;"
|
|
954
|
+
"border-bottom:1px solid var(--border);}"
|
|
955
|
+
".card-icon{"
|
|
956
|
+
"width:36px;height:36px;border-radius:9px;display:flex;"
|
|
957
|
+
"align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;}"
|
|
958
|
+
".card-title{font-size:.72rem;font-weight:700;text-transform:uppercase;"
|
|
959
|
+
"letter-spacing:.09em}"
|
|
960
|
+
".card-sub{font-size:.65rem;color:var(--muted);margin-top:2px}"
|
|
961
|
+
".tool-list{display:flex;flex-direction:column;gap:1px;padding:8px}"
|
|
962
|
+
".tool{display:flex;align-items:center;gap:9px;padding:8px 12px;"
|
|
963
|
+
"border-radius:8px;transition:background .15s;}"
|
|
964
|
+
".tool:hover{background:rgba(255,255,255,.03)}"
|
|
965
|
+
".dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}"
|
|
966
|
+
".tool-name{font-family:var(--mono);font-size:.76rem;font-weight:500;"
|
|
967
|
+
"color:#e2e8f0;flex:1;min-width:0}"
|
|
968
|
+
".tool-desc{font-size:.7rem;color:var(--muted);flex:2;min-width:0}"
|
|
969
|
+
".badge{font-size:.58rem;font-weight:700;padding:2px 8px;border-radius:999px;"
|
|
970
|
+
"flex-shrink:0;white-space:nowrap;font-family:var(--mono);letter-spacing:.02em;}"
|
|
971
|
+
".legend-wrap{"
|
|
972
|
+
"background:var(--bg3);border:1px solid var(--border);"
|
|
973
|
+
"border-radius:14px;padding:20px 24px;max-width:800px;margin:0 auto;}"
|
|
974
|
+
".legend-title{font-size:.62rem;font-weight:700;text-transform:uppercase;"
|
|
975
|
+
"letter-spacing:.12em;color:var(--muted);margin-bottom:14px;}"
|
|
976
|
+
".legend-grid{display:flex;flex-wrap:wrap;gap:10px 24px}"
|
|
977
|
+
".legend-item{display:flex;align-items:center;gap:8px;font-size:.75rem;color:#94a3b8}"
|
|
978
|
+
"footer{text-align:center;padding:28px 24px 40px;font-size:.68rem;"
|
|
979
|
+
"color:var(--dim);border-top:1px solid var(--border);margin-top:24px;}"
|
|
980
|
+
"@media(max-width:640px){"
|
|
981
|
+
".bar-lbl{width:140px}"
|
|
982
|
+
".tool-desc{display:none}"
|
|
983
|
+
".cards-grid{grid-template-columns:1fr}"
|
|
984
|
+
"h1{font-size:1.8rem}}"
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
return f"""<!DOCTYPE html>
|
|
988
|
+
<html lang="en">
|
|
989
|
+
<head>
|
|
990
|
+
<meta charset="UTF-8">
|
|
991
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
992
|
+
<title>{project_name} — Tech Stack</title>
|
|
993
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
994
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
995
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
996
|
+
<style>{css}</style>
|
|
997
|
+
</head>
|
|
998
|
+
<body>
|
|
999
|
+
|
|
1000
|
+
<header class="header">
|
|
1001
|
+
<div class="header-badge"><span class="badge-dot"></span>Auto-generated</div>
|
|
1002
|
+
<h1>{project_name}</h1>
|
|
1003
|
+
<p class="header-sub">Complete Technology Stack · {total} tools across {cat_count} categories · {today}</p>
|
|
1004
|
+
</header>
|
|
1005
|
+
|
|
1006
|
+
<div class="stats-wrap">
|
|
1007
|
+
{stats_html}
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
<div class="section">
|
|
1011
|
+
<div class="sec-hdr">
|
|
1012
|
+
<span class="sec-title">System Architecture</span>
|
|
1013
|
+
<div class="sec-line"></div>
|
|
1014
|
+
<span class="sec-title" style="color:#334155">how it fits together</span>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="arch">
|
|
1017
|
+
{arch_html}
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
|
|
1021
|
+
<div class="section">
|
|
1022
|
+
<div class="sec-hdr">
|
|
1023
|
+
<span class="sec-title">By the Numbers</span>
|
|
1024
|
+
<div class="sec-line"></div>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="chart-wrap">
|
|
1027
|
+
{bars_html}
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<div class="section">
|
|
1032
|
+
<div class="sec-hdr">
|
|
1033
|
+
<span class="sec-title">Full Stack</span>
|
|
1034
|
+
<div class="sec-line"></div>
|
|
1035
|
+
<span class="sec-title" style="color:#334155">{total} tools detected</span>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="cards-grid">
|
|
1038
|
+
{cards_html}
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<div class="section">
|
|
1043
|
+
<div class="legend-wrap">
|
|
1044
|
+
<div class="legend-title">Badge Reference</div>
|
|
1045
|
+
<div class="legend-grid">
|
|
1046
|
+
<div class="legend-item"><span class="badge" style="background:#1e293b;color:#94a3b8">pip</span>Installed via requirements.txt</div>
|
|
1047
|
+
<div class="legend-item"><span class="badge" style="background:#1e293b;color:#64748b">optional</span>Detected in source (install separately)</div>
|
|
1048
|
+
<div class="legend-item"><span class="badge" style="background:#14532d;color:#86efac">dep</span>npm dependency</div>
|
|
1049
|
+
<div class="legend-item"><span class="badge" style="background:#1e1b4b;color:#a5b4fc">devDep</span>npm dev dependency</div>
|
|
1050
|
+
<div class="legend-item"><span class="badge" style="background:#14532d;color:#86efac">core</span>Always present (embedded)</div>
|
|
1051
|
+
<div class="legend-item"><span class="badge" style="background:#431407;color:#fdba74">lang</span>Language / runtime</div>
|
|
1052
|
+
<div class="legend-item"><span class="badge" style="background:#3b1f0f;color:#fdba74">deploy</span>Deployment artifact</div>
|
|
1053
|
+
<div class="legend-item"><span class="badge" style="background:#1e1b4b;color:#a5b4fc">ci</span>CI/CD integration</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1058
|
+
<footer>
|
|
1059
|
+
{project_name} · {total} tools in {cat_count} categories · {today} · generate-tech-stack
|
|
1060
|
+
</footer>
|
|
1061
|
+
|
|
1062
|
+
</body>
|
|
1063
|
+
</html>"""
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
1067
|
+
|
|
1068
|
+
def main():
|
|
1069
|
+
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
|
|
1070
|
+
output = Path(sys.argv[2]) if len(sys.argv) > 2 else root / "TECH_STACK.html"
|
|
1071
|
+
|
|
1072
|
+
project_name = root.resolve().name.replace("-", " ").replace("_", " ").title()
|
|
1073
|
+
tools = collect(root)
|
|
1074
|
+
|
|
1075
|
+
if not tools:
|
|
1076
|
+
print("No recognizable dependency files found.", file=sys.stderr)
|
|
1077
|
+
sys.exit(1)
|
|
1078
|
+
|
|
1079
|
+
output.write_text(render_html(tools, project_name))
|
|
1080
|
+
|
|
1081
|
+
total = sum(len(v) for v in tools.values())
|
|
1082
|
+
print(f"Written: {output}")
|
|
1083
|
+
print(f"Detected {total} tools in {len(tools)} categories")
|
|
1084
|
+
for cat, items in tools.items():
|
|
1085
|
+
meta = CATEGORY_META.get(cat, CATEGORY_META["other"])
|
|
1086
|
+
print(f" {meta['icon']} {meta['label']}: {', '.join(t['name'] for t in items)}")
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
if __name__ == "__main__":
|
|
1090
|
+
main()
|