popeye-cli 1.6.0 → 1.8.0

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 (161) hide show
  1. package/README.md +240 -32
  2. package/cheatsheet.md +407 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +2 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +2 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/interactive.d.ts.map +1 -1
  19. package/dist/cli/interactive.js +96 -0
  20. package/dist/cli/interactive.js.map +1 -1
  21. package/dist/generators/admin-wizard.d.ts +25 -0
  22. package/dist/generators/admin-wizard.d.ts.map +1 -0
  23. package/dist/generators/admin-wizard.js +123 -0
  24. package/dist/generators/admin-wizard.js.map +1 -0
  25. package/dist/generators/all.d.ts.map +1 -1
  26. package/dist/generators/all.js +10 -3
  27. package/dist/generators/all.js.map +1 -1
  28. package/dist/generators/database.d.ts +58 -0
  29. package/dist/generators/database.d.ts.map +1 -0
  30. package/dist/generators/database.js +229 -0
  31. package/dist/generators/database.js.map +1 -0
  32. package/dist/generators/fullstack.d.ts.map +1 -1
  33. package/dist/generators/fullstack.js +23 -7
  34. package/dist/generators/fullstack.js.map +1 -1
  35. package/dist/generators/index.d.ts +2 -0
  36. package/dist/generators/index.d.ts.map +1 -1
  37. package/dist/generators/index.js +2 -0
  38. package/dist/generators/index.js.map +1 -1
  39. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  40. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  41. package/dist/generators/templates/admin-wizard-python.js +425 -0
  42. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  43. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  44. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-react.js +554 -0
  46. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  47. package/dist/generators/templates/database-docker.d.ts +23 -0
  48. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  49. package/dist/generators/templates/database-docker.js +221 -0
  50. package/dist/generators/templates/database-docker.js.map +1 -0
  51. package/dist/generators/templates/database-python.d.ts +54 -0
  52. package/dist/generators/templates/database-python.d.ts.map +1 -0
  53. package/dist/generators/templates/database-python.js +723 -0
  54. package/dist/generators/templates/database-python.js.map +1 -0
  55. package/dist/generators/templates/database-typescript.d.ts +34 -0
  56. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  57. package/dist/generators/templates/database-typescript.js +232 -0
  58. package/dist/generators/templates/database-typescript.js.map +1 -0
  59. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  60. package/dist/generators/templates/fullstack.js +29 -0
  61. package/dist/generators/templates/fullstack.js.map +1 -1
  62. package/dist/generators/templates/index.d.ts +5 -0
  63. package/dist/generators/templates/index.d.ts.map +1 -1
  64. package/dist/generators/templates/index.js +5 -0
  65. package/dist/generators/templates/index.js.map +1 -1
  66. package/dist/state/index.d.ts +10 -0
  67. package/dist/state/index.d.ts.map +1 -1
  68. package/dist/state/index.js +22 -0
  69. package/dist/state/index.js.map +1 -1
  70. package/dist/types/consensus.d.ts +3 -0
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +1 -0
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/database-runtime.d.ts +86 -0
  75. package/dist/types/database-runtime.d.ts.map +1 -0
  76. package/dist/types/database-runtime.js +61 -0
  77. package/dist/types/database-runtime.js.map +1 -0
  78. package/dist/types/database.d.ts +85 -0
  79. package/dist/types/database.d.ts.map +1 -0
  80. package/dist/types/database.js +71 -0
  81. package/dist/types/database.js.map +1 -0
  82. package/dist/types/index.d.ts +3 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +6 -0
  85. package/dist/types/index.js.map +1 -1
  86. package/dist/types/tester.d.ts +138 -0
  87. package/dist/types/tester.d.ts.map +1 -0
  88. package/dist/types/tester.js +110 -0
  89. package/dist/types/tester.js.map +1 -0
  90. package/dist/types/workflow.d.ts +166 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +14 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/db-setup-runner.d.ts +63 -0
  95. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  96. package/dist/workflow/db-setup-runner.js +336 -0
  97. package/dist/workflow/db-setup-runner.js.map +1 -0
  98. package/dist/workflow/db-state-machine.d.ts +30 -0
  99. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  100. package/dist/workflow/db-state-machine.js +51 -0
  101. package/dist/workflow/db-state-machine.js.map +1 -0
  102. package/dist/workflow/execution-mode.js +2 -2
  103. package/dist/workflow/execution-mode.js.map +1 -1
  104. package/dist/workflow/index.d.ts +3 -0
  105. package/dist/workflow/index.d.ts.map +1 -1
  106. package/dist/workflow/index.js +3 -0
  107. package/dist/workflow/index.js.map +1 -1
  108. package/dist/workflow/task-workflow.d.ts +5 -0
  109. package/dist/workflow/task-workflow.d.ts.map +1 -1
  110. package/dist/workflow/task-workflow.js +172 -6
  111. package/dist/workflow/task-workflow.js.map +1 -1
  112. package/dist/workflow/tester.d.ts +120 -0
  113. package/dist/workflow/tester.d.ts.map +1 -0
  114. package/dist/workflow/tester.js +589 -0
  115. package/dist/workflow/tester.js.map +1 -0
  116. package/dist/workflow/workflow-logger.d.ts +1 -1
  117. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  118. package/dist/workflow/workflow-logger.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/cli/commands/db.ts +281 -0
  121. package/src/cli/commands/doctor.ts +273 -0
  122. package/src/cli/commands/index.ts +2 -0
  123. package/src/cli/index.ts +4 -0
  124. package/src/cli/interactive.ts +102 -0
  125. package/src/generators/admin-wizard.ts +146 -0
  126. package/src/generators/all.ts +10 -3
  127. package/src/generators/database.ts +286 -0
  128. package/src/generators/fullstack.ts +26 -9
  129. package/src/generators/index.ts +12 -0
  130. package/src/generators/templates/admin-wizard-python.ts +431 -0
  131. package/src/generators/templates/admin-wizard-react.ts +560 -0
  132. package/src/generators/templates/database-docker.ts +227 -0
  133. package/src/generators/templates/database-python.ts +734 -0
  134. package/src/generators/templates/database-typescript.ts +238 -0
  135. package/src/generators/templates/fullstack.ts +29 -0
  136. package/src/generators/templates/index.ts +5 -0
  137. package/src/state/index.ts +29 -0
  138. package/src/types/consensus.ts +3 -0
  139. package/src/types/database-runtime.ts +69 -0
  140. package/src/types/database.ts +84 -0
  141. package/src/types/index.ts +50 -0
  142. package/src/types/tester.ts +136 -0
  143. package/src/types/workflow.ts +31 -0
  144. package/src/workflow/db-setup-runner.ts +391 -0
  145. package/src/workflow/db-state-machine.ts +58 -0
  146. package/src/workflow/execution-mode.ts +2 -2
  147. package/src/workflow/index.ts +3 -0
  148. package/src/workflow/task-workflow.ts +227 -5
  149. package/src/workflow/tester.ts +723 -0
  150. package/src/workflow/workflow-logger.ts +2 -0
  151. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  152. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  153. package/tests/generators/cross-phase-integration.test.ts +383 -0
  154. package/tests/generators/database.test.ts +456 -0
  155. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  156. package/tests/types/database-runtime.test.ts +158 -0
  157. package/tests/types/database.test.ts +187 -0
  158. package/tests/types/tester.test.ts +174 -0
  159. package/tests/workflow/db-setup-runner.test.ts +211 -0
  160. package/tests/workflow/db-state-machine.test.ts +117 -0
  161. package/tests/workflow/tester.test.ts +401 -0
@@ -69,6 +69,18 @@ export {
69
69
  getAllProjectFiles,
70
70
  type AllGeneratorOptions,
71
71
  } from './all.js';
72
+ export {
73
+ generatePythonDatabaseLayer,
74
+ generateTypeScriptDatabaseLayer,
75
+ augmentRequirements,
76
+ getDatabaseFiles,
77
+ DB_PYTHON_DEPS,
78
+ } from './database.js';
79
+ export {
80
+ generateAdminWizardLayer,
81
+ getAdminWizardFiles,
82
+ ADMIN_WIZARD_PYTHON_DEPS,
83
+ } from './admin-wizard.js';
72
84
  export * from './templates/index.js';
73
85
 
74
86
  /**
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Admin Wizard Python backend templates
3
+ * Generates FastAPI middleware and admin DB routes for the setup wizard
4
+ */
5
+
6
+ /**
7
+ * Generate admin auth middleware that validates X-Admin-Token header
8
+ *
9
+ * @returns Python source for admin_auth.py
10
+ */
11
+ export function generateAdminAuthMiddleware(): string {
12
+ return `"""
13
+ Admin authentication middleware.
14
+
15
+ Validates the X-Admin-Token header against the ADMIN_SETUP_TOKEN
16
+ environment variable. Used to protect admin setup endpoints.
17
+ """
18
+
19
+ import logging
20
+ import os
21
+
22
+ from fastapi import Header, HTTPException
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ async def require_admin_token(
28
+ x_admin_token: str = Header(..., alias="X-Admin-Token"),
29
+ ) -> str:
30
+ """
31
+ FastAPI dependency that validates the admin setup token.
32
+
33
+ Args:
34
+ x_admin_token (str): Token from X-Admin-Token header.
35
+
36
+ Returns:
37
+ str: The validated token.
38
+
39
+ Raises:
40
+ HTTPException: 403 if token is missing, invalid, or not configured.
41
+ """
42
+ expected = os.getenv("ADMIN_SETUP_TOKEN", "")
43
+ if not expected:
44
+ logger.warning("ADMIN_SETUP_TOKEN is not configured")
45
+ raise HTTPException(
46
+ status_code=403,
47
+ detail="Admin setup token is not configured on the server.",
48
+ )
49
+ if x_admin_token != expected:
50
+ logger.warning("Invalid admin token attempt")
51
+ raise HTTPException(
52
+ status_code=403,
53
+ detail="Invalid admin token.",
54
+ )
55
+ return x_admin_token
56
+ `;
57
+ }
58
+
59
+ /**
60
+ * Generate middleware package __init__.py
61
+ *
62
+ * @returns Python source for middleware/__init__.py
63
+ */
64
+ export function generateMiddlewareInit(): string {
65
+ return `"""
66
+ Middleware package.
67
+
68
+ Re-exports authentication dependencies.
69
+ """
70
+
71
+ from .admin_auth import require_admin_token
72
+
73
+ __all__ = ["require_admin_token"]
74
+ `;
75
+ }
76
+
77
+ /**
78
+ * Generate admin DB routes with 4 endpoints for the setup wizard
79
+ *
80
+ * @param packageName - Python package name (snake_case)
81
+ * @returns Python source for routes/admin_db.py
82
+ */
83
+ export function generateAdminDbRoutes(packageName: string): string {
84
+ return `"""
85
+ Admin database setup routes.
86
+
87
+ Provides endpoints for the admin wizard to configure and
88
+ initialize the database without using the CLI.
89
+ """
90
+
91
+ import logging
92
+ import os
93
+ import subprocess
94
+ from pathlib import Path
95
+
96
+ from fastapi import APIRouter, Depends
97
+ from pydantic import BaseModel
98
+
99
+ from ${packageName}.middleware.admin_auth import require_admin_token
100
+
101
+ logger = logging.getLogger(__name__)
102
+
103
+ router = APIRouter(
104
+ prefix="/api/admin/db",
105
+ tags=["admin"],
106
+ dependencies=[Depends(require_admin_token)],
107
+ )
108
+
109
+
110
+ class TestRequest(BaseModel):
111
+ """Request body for connection test."""
112
+
113
+ database_url: str
114
+
115
+
116
+ class ApplyRequest(BaseModel):
117
+ """Request body for applying database setup."""
118
+
119
+ database_url: str
120
+ mode: str = "default"
121
+
122
+
123
+ class StepResult(BaseModel):
124
+ """Result of a single setup step."""
125
+
126
+ step: str
127
+ success: bool
128
+ message: str
129
+
130
+
131
+ def _read_env_file() -> dict[str, str]:
132
+ """
133
+ Read key=value pairs from the backend .env file.
134
+
135
+ Returns:
136
+ dict[str, str]: Parsed environment variables.
137
+ """
138
+ env_path = Path(__file__).resolve().parents[3] / ".env"
139
+ pairs: dict[str, str] = {}
140
+ if env_path.exists():
141
+ for line in env_path.read_text().splitlines():
142
+ line = line.strip()
143
+ if line and not line.startswith("#") and "=" in line:
144
+ key, _, value = line.partition("=")
145
+ pairs[key.strip()] = value.strip()
146
+ return pairs
147
+
148
+
149
+ def _write_env_var(key: str, value: str) -> None:
150
+ """
151
+ Write or update a key in the backend .env file.
152
+
153
+ Args:
154
+ key (str): Environment variable name.
155
+ value (str): Environment variable value.
156
+ """
157
+ env_path = Path(__file__).resolve().parents[3] / ".env"
158
+ lines: list[str] = []
159
+ found = False
160
+ if env_path.exists():
161
+ for line in env_path.read_text().splitlines():
162
+ stripped = line.strip()
163
+ if stripped.startswith(f"{key}="):
164
+ lines.append(f"{key}={value}")
165
+ found = True
166
+ else:
167
+ lines.append(line)
168
+ if not found:
169
+ lines.append(f"{key}={value}")
170
+ env_path.write_text("\\n".join(lines) + "\\n")
171
+
172
+
173
+ def _run_alembic_upgrade() -> StepResult:
174
+ """
175
+ Run alembic upgrade head as a subprocess.
176
+
177
+ Returns:
178
+ StepResult: Result of the migration step.
179
+ """
180
+ backend_dir = Path(__file__).resolve().parents[3]
181
+ try:
182
+ result = subprocess.run(
183
+ ["alembic", "upgrade", "head"],
184
+ cwd=str(backend_dir),
185
+ capture_output=True,
186
+ text=True,
187
+ timeout=60,
188
+ )
189
+ if result.returncode == 0:
190
+ return StepResult(
191
+ step="migrate",
192
+ success=True,
193
+ message="Migrations applied successfully.",
194
+ )
195
+ logger.error(f"Alembic failed: {result.stderr}")
196
+ return StepResult(
197
+ step="migrate",
198
+ success=False,
199
+ message=f"Migration failed: {result.stderr[:500]}",
200
+ )
201
+ except subprocess.TimeoutExpired:
202
+ return StepResult(
203
+ step="migrate",
204
+ success=False,
205
+ message="Migration timed out after 60 seconds.",
206
+ )
207
+ except FileNotFoundError:
208
+ return StepResult(
209
+ step="migrate",
210
+ success=False,
211
+ message="Alembic not found. Install with: pip install alembic",
212
+ )
213
+
214
+
215
+ @router.get("/status")
216
+ async def db_status():
217
+ """
218
+ Get current database setup status.
219
+
220
+ Returns:
221
+ dict: Status information including connectivity and migration state.
222
+ """
223
+ import asyncpg
224
+
225
+ db_url = os.getenv("DATABASE_URL", "")
226
+ db_configured = bool(db_url)
227
+ status = "unconfigured"
228
+ last_error = None
229
+ migrations_applied = 0
230
+
231
+ if db_configured:
232
+ # Convert SQLAlchemy URL to asyncpg format if needed
233
+ connect_url = db_url.replace("postgresql+asyncpg://", "postgresql://")
234
+ try:
235
+ conn = await asyncpg.connect(connect_url)
236
+ try:
237
+ # Check alembic_version table
238
+ row = await conn.fetchval(
239
+ "SELECT COUNT(*) FROM alembic_version"
240
+ )
241
+ migrations_applied = row or 0
242
+ status = "ready"
243
+ except asyncpg.UndefinedTableError:
244
+ status = "configured"
245
+ finally:
246
+ await conn.close()
247
+ except Exception as exc:
248
+ logger.error(f"DB status check failed: {exc}")
249
+ status = "error"
250
+ last_error = str(exc)
251
+
252
+ return {
253
+ "status": status,
254
+ "mode": "default",
255
+ "lastError": last_error,
256
+ "migrationsApplied": migrations_applied,
257
+ "dbUrlConfigured": db_configured,
258
+ }
259
+
260
+
261
+ @router.post("/test")
262
+ async def test_connection(body: TestRequest):
263
+ """
264
+ Test database connectivity without saving the URL.
265
+
266
+ Args:
267
+ body (TestRequest): Contains the database_url to test.
268
+
269
+ Returns:
270
+ dict: Success flag and message.
271
+ """
272
+ import asyncpg
273
+
274
+ connect_url = body.database_url.replace(
275
+ "postgresql+asyncpg://", "postgresql://"
276
+ )
277
+ try:
278
+ conn = await asyncpg.connect(connect_url)
279
+ await conn.execute("SELECT 1")
280
+ await conn.close()
281
+ return {"success": True, "message": "Connection successful."}
282
+ except Exception as exc:
283
+ logger.error(f"Connection test failed: {exc}")
284
+ return {"success": False, "message": str(exc)}
285
+
286
+
287
+ @router.post("/apply")
288
+ async def apply_setup(body: ApplyRequest):
289
+ """
290
+ Save DATABASE_URL to .env and run migrations.
291
+
292
+ Args:
293
+ body (ApplyRequest): Contains database_url and optional mode.
294
+
295
+ Returns:
296
+ dict: List of step results and final status.
297
+ """
298
+ steps: list[dict] = []
299
+
300
+ # Step 1: Write DATABASE_URL to .env
301
+ try:
302
+ _write_env_var("DATABASE_URL", body.database_url)
303
+ os.environ["DATABASE_URL"] = body.database_url
304
+ steps.append({"step": "save_url", "success": True, "message": "DATABASE_URL saved."})
305
+ except Exception as exc:
306
+ steps.append({"step": "save_url", "success": False, "message": str(exc)})
307
+ return {"steps": steps, "status": "error"}
308
+
309
+ # Step 2: Run migrations
310
+ migrate_result = _run_alembic_upgrade()
311
+ steps.append(migrate_result.model_dump())
312
+
313
+ final_status = "ready" if migrate_result.success else "error"
314
+ return {"steps": steps, "status": final_status}
315
+
316
+
317
+ @router.post("/retry")
318
+ async def retry_setup():
319
+ """
320
+ Re-run migrations using the existing DATABASE_URL from .env.
321
+
322
+ Returns:
323
+ dict: List of step results and final status.
324
+ """
325
+ steps: list[dict] = []
326
+
327
+ # Read DATABASE_URL from .env
328
+ env_vars = _read_env_file()
329
+ db_url = env_vars.get("DATABASE_URL", "")
330
+ if not db_url:
331
+ return {
332
+ "steps": [
333
+ {"step": "read_env", "success": False, "message": "DATABASE_URL not found in .env"}
334
+ ],
335
+ "status": "error",
336
+ }
337
+
338
+ os.environ["DATABASE_URL"] = db_url
339
+ steps.append({"step": "read_env", "success": True, "message": "DATABASE_URL loaded from .env."})
340
+
341
+ # Run migrations
342
+ migrate_result = _run_alembic_upgrade()
343
+ steps.append(migrate_result.model_dump())
344
+
345
+ final_status = "ready" if migrate_result.success else "error"
346
+ return {"steps": steps, "status": final_status}
347
+ `;
348
+ }
349
+
350
+ /**
351
+ * Generate extended FastAPI main.py that includes admin and health DB routers
352
+ *
353
+ * @param projectName - Human-readable project name
354
+ * @param packageName - Python package name (snake_case)
355
+ * @returns Python source for main.py
356
+ */
357
+ export function generateFastAPIMainWithAdmin(
358
+ projectName: string,
359
+ packageName: string
360
+ ): string {
361
+ return `"""
362
+ ${projectName} Backend API
363
+
364
+ FastAPI application entry point with admin wizard and database health routes.
365
+ """
366
+
367
+ import logging
368
+ from fastapi import FastAPI
369
+ from fastapi.middleware.cors import CORSMiddleware
370
+
371
+ from ${packageName}.routes.admin_db import router as admin_db_router
372
+ from ${packageName}.routes.health_db import router as health_db_router
373
+
374
+ # Configure logging
375
+ logging.basicConfig(level=logging.INFO)
376
+ logger = logging.getLogger(__name__)
377
+
378
+ # Create FastAPI app
379
+ app = FastAPI(
380
+ title="${projectName} API",
381
+ description="Backend API for ${projectName}",
382
+ version="1.0.0",
383
+ )
384
+
385
+ # Configure CORS
386
+ app.add_middleware(
387
+ CORSMiddleware,
388
+ allow_origins=["http://localhost:5173", "http://localhost:3000"],
389
+ allow_credentials=True,
390
+ allow_methods=["*"],
391
+ allow_headers=["*"],
392
+ )
393
+
394
+ # Include routers
395
+ app.include_router(admin_db_router)
396
+ app.include_router(health_db_router)
397
+
398
+
399
+ @app.get("/health")
400
+ async def health_check():
401
+ """
402
+ Health check endpoint.
403
+
404
+ Returns:
405
+ dict: Health status.
406
+ """
407
+ return {
408
+ "status": "healthy",
409
+ "message": "Backend is running",
410
+ }
411
+
412
+
413
+ @app.get("/")
414
+ async def root():
415
+ """
416
+ Root endpoint.
417
+
418
+ Returns:
419
+ dict: Welcome message.
420
+ """
421
+ return {
422
+ "message": "Welcome to ${projectName} API",
423
+ "docs": "/docs",
424
+ }
425
+
426
+
427
+ if __name__ == "__main__":
428
+ import uvicorn
429
+ uvicorn.run(app, host="0.0.0.0", port=8000)
430
+ `;
431
+ }