wasm-cli 0.14.1__tar.gz → 0.14.2__tar.gz
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.
- {wasm_cli-0.14.1/src/wasm_cli.egg-info → wasm_cli-0.14.2}/PKG-INFO +1 -1
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/pyproject.toml +1 -1
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/setup.py +1 -1
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/__init__.py +1 -1
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/webapp.py +67 -9
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/parser.py +22 -3
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/__init__.py +2 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/logger.py +2 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/store.py +26 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/__init__.py +2 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/helpers/__init__.py +4 -0
- wasm_cli-0.14.2/src/wasm/deployers/helpers/turbo.py +337 -0
- wasm_cli-0.14.2/src/wasm/deployers/helpers/workspace.py +423 -0
- wasm_cli-0.14.2/src/wasm/deployers/monorepo.py +1243 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/registry.py +2 -0
- wasm_cli-0.14.2/src/wasm/templates/nginx/monorepo.conf.j2 +144 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm_cli.egg-info/SOURCES.txt +4 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/LICENSE +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/MANIFEST.in +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/README.md +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/docs/MONITOR.md +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/docs/assets/logo.png +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/man/wasm.1 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/setup.cfg +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/__main__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/backup.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/cert.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/db.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/health.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/monitor.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/commands/web.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/cli/interactive.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/completions/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/config.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/dependencies.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/exceptions.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/core/utils.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/base.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/python.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/main.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/backup_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/base_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/cert_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/postgres.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/redis.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/nginx_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/service_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/monitor/ai_analyzer.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/monitor/process_monitor.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/monitor/threat_store.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/validators/port.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/validators/source.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/apps.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/auth.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/router.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/services.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/sites.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/auth.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/jobs.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/server.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/css/main.css +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/index.html +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/components/cards.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/components/jobs.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/components/metrics.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/components/skeleton.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/api.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/dialogs.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/notifications.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/router.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/search.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/shortcuts.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/theme.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/ui.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/core/websocket.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/main.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/apps.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/backups.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/certs.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/config.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/dashboard.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/databases.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/jobs.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/logs.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/monitor.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/services.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/js/pages/sites.js +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/login.html +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/static/logo.webp +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm/web/websockets/router.py +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-0.14.1 → wasm_cli-0.14.2}/src/wasm_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wasm-cli"
|
|
7
|
-
version = "0.14.
|
|
7
|
+
version = "0.14.2"
|
|
8
8
|
description = "Web App System Management - Deploy and manage web applications on Linux servers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "WASM-NCSAL" }
|
|
@@ -9,7 +9,7 @@ from setuptools import setup, find_packages
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="wasm-cli",
|
|
12
|
-
version="0.14.
|
|
12
|
+
version="0.14.2",
|
|
13
13
|
description="Web App System Management - Deploy and manage web applications on Linux servers",
|
|
14
14
|
author="Yago López Prado",
|
|
15
15
|
author_email="yago.lopez.adeje@gmail.com",
|
|
@@ -202,10 +202,14 @@ def _handle_create(args: Namespace) -> int:
|
|
|
202
202
|
logger.key_value("Package Manager", package_manager)
|
|
203
203
|
logger.key_value("SSL", "Yes" if not args.no_ssl else "No")
|
|
204
204
|
logger.blank()
|
|
205
|
-
|
|
205
|
+
|
|
206
|
+
# Handle monorepo deployments specially
|
|
207
|
+
if app_type == "monorepo":
|
|
208
|
+
return _handle_monorepo_create(args, domain, env_vars, logger)
|
|
209
|
+
|
|
206
210
|
# Get deployer
|
|
207
211
|
deployer = get_deployer(app_type, verbose=args.verbose)
|
|
208
|
-
|
|
212
|
+
|
|
209
213
|
# Configure deployer
|
|
210
214
|
deployer.configure(
|
|
211
215
|
domain=domain,
|
|
@@ -217,10 +221,51 @@ def _handle_create(args: Namespace) -> int:
|
|
|
217
221
|
env_vars=env_vars,
|
|
218
222
|
package_manager=package_manager,
|
|
219
223
|
)
|
|
220
|
-
|
|
224
|
+
|
|
221
225
|
# Run deployment
|
|
222
226
|
deployer.deploy()
|
|
223
|
-
|
|
227
|
+
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _handle_monorepo_create(args: Namespace, domain: str, env_vars: Dict, logger: Logger) -> int:
|
|
232
|
+
"""Handle monorepo deployment specially."""
|
|
233
|
+
from wasm.deployers.monorepo import MonorepoDeployer
|
|
234
|
+
|
|
235
|
+
# Parse subdomain overrides
|
|
236
|
+
subdomain_overrides = {}
|
|
237
|
+
if getattr(args, "subdomains", None):
|
|
238
|
+
for mapping in args.subdomains:
|
|
239
|
+
if ":" in mapping:
|
|
240
|
+
app_name, subdomain = mapping.split(":", 1)
|
|
241
|
+
subdomain_overrides[app_name] = subdomain
|
|
242
|
+
else:
|
|
243
|
+
logger.warning(f"Invalid subdomain mapping: {mapping} (expected app:subdomain)")
|
|
244
|
+
|
|
245
|
+
# Get workspace filter
|
|
246
|
+
workspace_filter = getattr(args, "workspaces", None)
|
|
247
|
+
|
|
248
|
+
# Get skip_database flag
|
|
249
|
+
skip_database = getattr(args, "no_database", False)
|
|
250
|
+
|
|
251
|
+
# Create and configure deployer
|
|
252
|
+
deployer = MonorepoDeployer(verbose=args.verbose)
|
|
253
|
+
|
|
254
|
+
deployer.configure(
|
|
255
|
+
domain=domain,
|
|
256
|
+
source=args.source,
|
|
257
|
+
webserver=args.webserver,
|
|
258
|
+
ssl=not args.no_ssl,
|
|
259
|
+
branch=args.branch,
|
|
260
|
+
env_vars=env_vars,
|
|
261
|
+
subdomain_overrides=subdomain_overrides,
|
|
262
|
+
workspace_filter=workspace_filter,
|
|
263
|
+
skip_database=skip_database,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Run deployment
|
|
267
|
+
deployer.deploy()
|
|
268
|
+
|
|
224
269
|
return 0
|
|
225
270
|
|
|
226
271
|
|
|
@@ -457,7 +502,7 @@ def _handle_start(args: Namespace) -> int:
|
|
|
457
502
|
def _handle_update(args: Namespace) -> int:
|
|
458
503
|
"""
|
|
459
504
|
Handle webapp update command with zero-downtime strategy.
|
|
460
|
-
|
|
505
|
+
|
|
461
506
|
Strategy:
|
|
462
507
|
1. Create pre-update backup (for rollback)
|
|
463
508
|
2. Pull/fetch new code
|
|
@@ -468,11 +513,24 @@ def _handle_update(args: Namespace) -> int:
|
|
|
468
513
|
"""
|
|
469
514
|
logger = Logger(verbose=args.verbose)
|
|
470
515
|
config = Config()
|
|
471
|
-
|
|
516
|
+
|
|
517
|
+
from wasm.core.store import get_store
|
|
518
|
+
store = get_store()
|
|
519
|
+
|
|
472
520
|
domain = validate_domain(args.domain)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
521
|
+
|
|
522
|
+
# Consultar BD primero para obtener el app_path real
|
|
523
|
+
app = store.get_app(domain)
|
|
524
|
+
|
|
525
|
+
if app and app.app_path:
|
|
526
|
+
# Usar el path almacenado en la BD (soporta apps legacy con prefijo wasm-)
|
|
527
|
+
app_path = Path(app.app_path)
|
|
528
|
+
app_name = app_path.name
|
|
529
|
+
else:
|
|
530
|
+
# Fallback para apps no registradas en BD
|
|
531
|
+
app_name = domain_to_app_name(domain)
|
|
532
|
+
app_path = config.apps_directory / app_name
|
|
533
|
+
|
|
476
534
|
if not app_path.exists():
|
|
477
535
|
raise WASMError(f"Application not found: {domain}")
|
|
478
536
|
|
|
@@ -36,7 +36,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
36
36
|
CLI tool for deploying and managing web applications on Linux servers.
|
|
37
37
|
Automates: Git clone, build, systemd, Nginx/Apache, SSL (Let's Encrypt).
|
|
38
38
|
|
|
39
|
-
Supported app types: nextjs, nodejs, vite, python, static
|
|
39
|
+
Supported app types: nextjs, nodejs, vite, python, static, monorepo
|
|
40
40
|
|
|
41
41
|
QUICK START:
|
|
42
42
|
wasm setup init Initialize directories (first time, requires sudo)
|
|
@@ -179,7 +179,7 @@ def _add_webapp_commands(subparsers) -> None:
|
|
|
179
179
|
)
|
|
180
180
|
create.add_argument(
|
|
181
181
|
"--type", "-t",
|
|
182
|
-
choices=["nextjs", "nodejs", "vite", "python", "static", "auto"],
|
|
182
|
+
choices=["nextjs", "nodejs", "vite", "python", "static", "monorepo", "auto"],
|
|
183
183
|
default="auto",
|
|
184
184
|
help="Application type (default: auto-detect)",
|
|
185
185
|
)
|
|
@@ -213,7 +213,26 @@ def _add_webapp_commands(subparsers) -> None:
|
|
|
213
213
|
default="auto",
|
|
214
214
|
help="Package manager to use (default: auto-detect)",
|
|
215
215
|
)
|
|
216
|
-
|
|
216
|
+
|
|
217
|
+
# Monorepo-specific options
|
|
218
|
+
create.add_argument(
|
|
219
|
+
"--subdomains",
|
|
220
|
+
nargs="+",
|
|
221
|
+
metavar="APP:SUBDOMAIN",
|
|
222
|
+
help="Subdomain mapping for monorepo apps (e.g., erp-backend:api web-gateway:app)",
|
|
223
|
+
)
|
|
224
|
+
create.add_argument(
|
|
225
|
+
"--workspaces",
|
|
226
|
+
nargs="+",
|
|
227
|
+
metavar="NAME",
|
|
228
|
+
help="Specific workspace apps to deploy (default: all)",
|
|
229
|
+
)
|
|
230
|
+
create.add_argument(
|
|
231
|
+
"--no-database",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help="Skip database provisioning for monorepo",
|
|
234
|
+
)
|
|
235
|
+
|
|
217
236
|
# list (alias: ls)
|
|
218
237
|
subparsers.add_parser(
|
|
219
238
|
"list",
|
|
@@ -11,6 +11,7 @@ from wasm.core.store import (
|
|
|
11
11
|
Service,
|
|
12
12
|
Database,
|
|
13
13
|
DatabaseUser,
|
|
14
|
+
MonorepoWorkspace,
|
|
14
15
|
AppType,
|
|
15
16
|
AppStatus,
|
|
16
17
|
WebServer,
|
|
@@ -28,6 +29,7 @@ __all__ = [
|
|
|
28
29
|
"Service",
|
|
29
30
|
"Database",
|
|
30
31
|
"DatabaseUser",
|
|
32
|
+
"MonorepoWorkspace",
|
|
31
33
|
"AppType",
|
|
32
34
|
"AppStatus",
|
|
33
35
|
"WebServer",
|
|
@@ -35,6 +35,7 @@ class AppType(str, Enum):
|
|
|
35
35
|
PYTHON = "python"
|
|
36
36
|
VITE = "vite"
|
|
37
37
|
STATIC = "static"
|
|
38
|
+
MONOREPO = "monorepo"
|
|
38
39
|
UNKNOWN = "unknown"
|
|
39
40
|
|
|
40
41
|
|
|
@@ -211,6 +212,31 @@ class DatabaseUser:
|
|
|
211
212
|
return cls(**dict(row))
|
|
212
213
|
|
|
213
214
|
|
|
215
|
+
@dataclass
|
|
216
|
+
class MonorepoWorkspace:
|
|
217
|
+
"""
|
|
218
|
+
Configuration for a workspace app within a monorepo.
|
|
219
|
+
|
|
220
|
+
Used by MonorepoDeployer to track individual apps in a Turborepo/pnpm
|
|
221
|
+
workspace monorepo.
|
|
222
|
+
"""
|
|
223
|
+
name: str = ""
|
|
224
|
+
path: str = ""
|
|
225
|
+
app_type: str = AppType.UNKNOWN.value
|
|
226
|
+
subdomain: str = ""
|
|
227
|
+
port: int = 3000
|
|
228
|
+
start_command: Optional[str] = None
|
|
229
|
+
health_check: str = "/"
|
|
230
|
+
env_vars: Dict[str, str] = field(default_factory=dict)
|
|
231
|
+
|
|
232
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
233
|
+
"""Convert to dictionary."""
|
|
234
|
+
d = asdict(self)
|
|
235
|
+
if isinstance(d.get('env_vars'), dict):
|
|
236
|
+
d['env_vars'] = json.dumps(d['env_vars'])
|
|
237
|
+
return d
|
|
238
|
+
|
|
239
|
+
|
|
214
240
|
# Schema version for migrations
|
|
215
241
|
SCHEMA_VERSION = 1
|
|
216
242
|
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from wasm.deployers.base import BaseDeployer
|
|
4
4
|
from wasm.deployers.registry import DeployerRegistry, get_deployer, detect_app_type
|
|
5
|
+
from wasm.deployers.monorepo import MonorepoDeployer
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"BaseDeployer",
|
|
8
9
|
"DeployerRegistry",
|
|
9
10
|
"get_deployer",
|
|
10
11
|
"detect_app_type",
|
|
12
|
+
"MonorepoDeployer",
|
|
11
13
|
]
|
|
@@ -12,9 +12,13 @@ to improve maintainability and testability.
|
|
|
12
12
|
from wasm.deployers.helpers.package_manager import PackageManagerHelper
|
|
13
13
|
from wasm.deployers.helpers.path_resolver import PathResolver
|
|
14
14
|
from wasm.deployers.helpers.prisma import PrismaHelper
|
|
15
|
+
from wasm.deployers.helpers.workspace import WorkspaceHelper
|
|
16
|
+
from wasm.deployers.helpers.turbo import TurboHelper
|
|
15
17
|
|
|
16
18
|
__all__ = [
|
|
17
19
|
"PackageManagerHelper",
|
|
18
20
|
"PathResolver",
|
|
19
21
|
"PrismaHelper",
|
|
22
|
+
"WorkspaceHelper",
|
|
23
|
+
"TurboHelper",
|
|
20
24
|
]
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Yago Lopez Prado
|
|
2
|
+
# Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
|
|
3
|
+
# https://github.com/Perkybeet/wasm/blob/main/LICENSE
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Turborepo helper for monorepo deployers.
|
|
7
|
+
|
|
8
|
+
Handles parsing of turbo.json configuration and provides build
|
|
9
|
+
pipeline information for monorepo deployments.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Set
|
|
15
|
+
|
|
16
|
+
from wasm.core.logger import Logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TurboHelper:
|
|
20
|
+
"""
|
|
21
|
+
Helper for Turborepo configuration analysis.
|
|
22
|
+
|
|
23
|
+
Provides parsing of turbo.json, build pipeline detection,
|
|
24
|
+
and dependency order calculation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, logger: Optional[Logger] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize Turborepo helper.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
logger: Logger instance for output.
|
|
33
|
+
"""
|
|
34
|
+
self.logger = logger or Logger()
|
|
35
|
+
self._config: Optional[Dict] = None
|
|
36
|
+
self._config_path: Optional[Path] = None
|
|
37
|
+
|
|
38
|
+
def detect(self, app_path: Path) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Detect if the project uses Turborepo.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
app_path: Root path of the project.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if Turborepo is detected.
|
|
47
|
+
"""
|
|
48
|
+
turbo_json = app_path / "turbo.json"
|
|
49
|
+
if turbo_json.exists():
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
# Also check package.json for turbo field
|
|
53
|
+
package_json = app_path / "package.json"
|
|
54
|
+
if package_json.exists():
|
|
55
|
+
try:
|
|
56
|
+
with open(package_json) as f:
|
|
57
|
+
pkg = json.load(f)
|
|
58
|
+
return "turbo" in pkg
|
|
59
|
+
except (json.JSONDecodeError, OSError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def load_config(self, app_path: Path) -> Dict:
|
|
65
|
+
"""
|
|
66
|
+
Load and parse turbo.json configuration.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
app_path: Root path of the project.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Parsed configuration dictionary.
|
|
73
|
+
"""
|
|
74
|
+
if self._config and self._config_path == app_path:
|
|
75
|
+
return self._config
|
|
76
|
+
|
|
77
|
+
turbo_json = app_path / "turbo.json"
|
|
78
|
+
|
|
79
|
+
if not turbo_json.exists():
|
|
80
|
+
self.logger.debug("No turbo.json found, using defaults")
|
|
81
|
+
self._config = self._default_config()
|
|
82
|
+
self._config_path = app_path
|
|
83
|
+
return self._config
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(turbo_json) as f:
|
|
87
|
+
self._config = json.load(f)
|
|
88
|
+
self._config_path = app_path
|
|
89
|
+
return self._config
|
|
90
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
91
|
+
self.logger.warning(f"Error reading turbo.json: {e}")
|
|
92
|
+
self._config = self._default_config()
|
|
93
|
+
self._config_path = app_path
|
|
94
|
+
return self._config
|
|
95
|
+
|
|
96
|
+
def _default_config(self) -> Dict:
|
|
97
|
+
"""
|
|
98
|
+
Return default Turborepo configuration.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Default config dictionary.
|
|
102
|
+
"""
|
|
103
|
+
return {
|
|
104
|
+
"tasks": {
|
|
105
|
+
"build": {
|
|
106
|
+
"dependsOn": ["^build"],
|
|
107
|
+
"outputs": [".next/**", "dist/**"],
|
|
108
|
+
},
|
|
109
|
+
"dev": {
|
|
110
|
+
"cache": False,
|
|
111
|
+
"persistent": True,
|
|
112
|
+
},
|
|
113
|
+
"start": {
|
|
114
|
+
"dependsOn": ["build"],
|
|
115
|
+
"cache": False,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def get_build_command(self, app_path: Path) -> List[str]:
|
|
121
|
+
"""
|
|
122
|
+
Get the build command for the monorepo.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
app_path: Root path of the project.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Build command as list of strings.
|
|
129
|
+
"""
|
|
130
|
+
config = self.load_config(app_path)
|
|
131
|
+
|
|
132
|
+
# Check if build task exists
|
|
133
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
134
|
+
has_build = "build" in tasks
|
|
135
|
+
|
|
136
|
+
if has_build:
|
|
137
|
+
return ["pnpm", "build"]
|
|
138
|
+
|
|
139
|
+
# Fallback to turbo run build
|
|
140
|
+
return ["pnpm", "exec", "turbo", "run", "build"]
|
|
141
|
+
|
|
142
|
+
def get_build_outputs(self, app_path: Path) -> List[str]:
|
|
143
|
+
"""
|
|
144
|
+
Get expected build output directories.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
app_path: Root path of the project.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of output directory patterns.
|
|
151
|
+
"""
|
|
152
|
+
config = self.load_config(app_path)
|
|
153
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
154
|
+
|
|
155
|
+
build_config = tasks.get("build", {})
|
|
156
|
+
outputs = build_config.get("outputs", [])
|
|
157
|
+
|
|
158
|
+
# Default outputs if not specified
|
|
159
|
+
if not outputs:
|
|
160
|
+
outputs = [".next/**", "dist/**", "build/**"]
|
|
161
|
+
|
|
162
|
+
return outputs
|
|
163
|
+
|
|
164
|
+
def get_task_dependencies(self, task_name: str, app_path: Path) -> List[str]:
|
|
165
|
+
"""
|
|
166
|
+
Get dependencies for a specific task.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
task_name: Name of the task (e.g., "build", "start").
|
|
170
|
+
app_path: Root path of the project.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of dependent task names.
|
|
174
|
+
"""
|
|
175
|
+
config = self.load_config(app_path)
|
|
176
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
177
|
+
|
|
178
|
+
task_config = tasks.get(task_name, {})
|
|
179
|
+
depends_on = task_config.get("dependsOn", [])
|
|
180
|
+
|
|
181
|
+
# Filter out topological dependencies (^)
|
|
182
|
+
direct_deps = [d.lstrip("^") for d in depends_on if not d.startswith("$")]
|
|
183
|
+
|
|
184
|
+
return direct_deps
|
|
185
|
+
|
|
186
|
+
def get_global_dependencies(self, app_path: Path) -> List[str]:
|
|
187
|
+
"""
|
|
188
|
+
Get global dependencies that affect all tasks.
|
|
189
|
+
|
|
190
|
+
These are files/directories that, when changed, should
|
|
191
|
+
invalidate all caches.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
app_path: Root path of the project.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of global dependency patterns.
|
|
198
|
+
"""
|
|
199
|
+
config = self.load_config(app_path)
|
|
200
|
+
|
|
201
|
+
# Turbo 2.x format
|
|
202
|
+
global_deps = config.get("globalDependencies", [])
|
|
203
|
+
|
|
204
|
+
# Common global dependencies
|
|
205
|
+
if not global_deps:
|
|
206
|
+
global_deps = [
|
|
207
|
+
"pnpm-lock.yaml",
|
|
208
|
+
"package.json",
|
|
209
|
+
"tsconfig.json",
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
return global_deps
|
|
213
|
+
|
|
214
|
+
def get_env_vars(self, app_path: Path) -> Set[str]:
|
|
215
|
+
"""
|
|
216
|
+
Get environment variables that should be considered for builds.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
app_path: Root path of the project.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Set of environment variable names.
|
|
223
|
+
"""
|
|
224
|
+
config = self.load_config(app_path)
|
|
225
|
+
|
|
226
|
+
env_vars = set()
|
|
227
|
+
|
|
228
|
+
# Global env (Turbo 2.x)
|
|
229
|
+
global_env = config.get("globalEnv", [])
|
|
230
|
+
env_vars.update(global_env)
|
|
231
|
+
|
|
232
|
+
# Task-specific env
|
|
233
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
234
|
+
for task_config in tasks.values():
|
|
235
|
+
if isinstance(task_config, dict):
|
|
236
|
+
task_env = task_config.get("env", [])
|
|
237
|
+
env_vars.update(task_env)
|
|
238
|
+
|
|
239
|
+
return env_vars
|
|
240
|
+
|
|
241
|
+
def is_cacheable(self, task_name: str, app_path: Path) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
Check if a task should be cached.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
task_name: Name of the task.
|
|
247
|
+
app_path: Root path of the project.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if the task is cacheable.
|
|
251
|
+
"""
|
|
252
|
+
config = self.load_config(app_path)
|
|
253
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
254
|
+
|
|
255
|
+
task_config = tasks.get(task_name, {})
|
|
256
|
+
|
|
257
|
+
# Cache is enabled by default unless explicitly disabled
|
|
258
|
+
return task_config.get("cache", True) is not False
|
|
259
|
+
|
|
260
|
+
def get_filter_args(self, workspaces: List[str]) -> List[str]:
|
|
261
|
+
"""
|
|
262
|
+
Generate filter arguments for turbo to only build specific workspaces.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
workspaces: List of workspace names to include.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of filter arguments for turbo command.
|
|
269
|
+
"""
|
|
270
|
+
if not workspaces:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
args = []
|
|
274
|
+
for workspace in workspaces:
|
|
275
|
+
args.extend(["--filter", workspace])
|
|
276
|
+
|
|
277
|
+
return args
|
|
278
|
+
|
|
279
|
+
def estimate_build_timeout(self, app_path: Path, workspace_count: int) -> int:
|
|
280
|
+
"""
|
|
281
|
+
Estimate build timeout based on project size.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
app_path: Root path of the project.
|
|
285
|
+
workspace_count: Number of workspaces to build.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Recommended timeout in seconds.
|
|
289
|
+
"""
|
|
290
|
+
# Base timeout: 5 minutes
|
|
291
|
+
base_timeout = 300
|
|
292
|
+
|
|
293
|
+
# Add 3 minutes per workspace
|
|
294
|
+
workspace_timeout = workspace_count * 180
|
|
295
|
+
|
|
296
|
+
# Cap at 30 minutes
|
|
297
|
+
return min(base_timeout + workspace_timeout, 1800)
|
|
298
|
+
|
|
299
|
+
def get_parallel_count(self) -> Optional[int]:
|
|
300
|
+
"""
|
|
301
|
+
Get recommended parallelization count for builds.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Number of parallel builds, or None for default.
|
|
305
|
+
"""
|
|
306
|
+
# Let Turborepo decide based on available CPUs
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def validate_config(self, app_path: Path) -> List[str]:
|
|
310
|
+
"""
|
|
311
|
+
Validate turbo.json configuration.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
app_path: Root path of the project.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of validation warnings (empty if valid).
|
|
318
|
+
"""
|
|
319
|
+
warnings = []
|
|
320
|
+
config = self.load_config(app_path)
|
|
321
|
+
|
|
322
|
+
# Check for deprecated pipeline key
|
|
323
|
+
if "pipeline" in config and "tasks" not in config:
|
|
324
|
+
warnings.append(
|
|
325
|
+
"turbo.json uses deprecated 'pipeline' key. "
|
|
326
|
+
"Consider upgrading to 'tasks' (Turbo 2.x)."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Check for build task
|
|
330
|
+
tasks = config.get("tasks", config.get("pipeline", {}))
|
|
331
|
+
if "build" not in tasks:
|
|
332
|
+
warnings.append(
|
|
333
|
+
"No 'build' task defined in turbo.json. "
|
|
334
|
+
"Build command may not work as expected."
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return warnings
|