skeleton-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. skeleton/__init__.py +0 -0
  2. skeleton/cli.py +151 -0
  3. skeleton/client.py +269 -0
  4. skeleton/infrastructure/__init__.py +1 -0
  5. skeleton/infrastructure/bootstrap/__init__.py +0 -0
  6. skeleton/infrastructure/bootstrap/src/__init__.py +0 -0
  7. skeleton/infrastructure/bootstrap/src/commands/__init__.py +0 -0
  8. skeleton/infrastructure/bootstrap/src/commands/client/__init__.py +0 -0
  9. skeleton/infrastructure/bootstrap/src/commands/client/init.py +486 -0
  10. skeleton/infrastructure/bootstrap/src/commands/client/install.py +113 -0
  11. skeleton/infrastructure/bootstrap/src/commands/dev/__init__.py +0 -0
  12. skeleton/infrastructure/bootstrap/src/commands/dev/build.py +80 -0
  13. skeleton/infrastructure/bootstrap/src/commands/dev/init.py +80 -0
  14. skeleton/infrastructure/bootstrap/src/commands/dev/install.py +168 -0
  15. skeleton/infrastructure/bootstrap/src/commands/dev/pack.py +55 -0
  16. skeleton/infrastructure/bootstrap/src/config.py +54 -0
  17. skeleton/infrastructure/bootstrap/src/copy.py +56 -0
  18. skeleton/infrastructure/bootstrap/src/load.py +221 -0
  19. skeleton/infrastructure/bootstrap/src/templates/app/backend/requirements.txt +1 -0
  20. skeleton/infrastructure/bootstrap/src/templates/app/backend/routes.json +10 -0
  21. skeleton/infrastructure/bootstrap/src/templates/app/backend/src/__init__.py +0 -0
  22. skeleton/infrastructure/bootstrap/src/templates/app/backend/src/main.py +35 -0
  23. skeleton/infrastructure/bootstrap/src/templates/app/frontend/package.json +21 -0
  24. skeleton/infrastructure/bootstrap/src/templates/app/frontend/skeleton-config.json +29 -0
  25. skeleton/infrastructure/bootstrap/src/templates/app/frontend/src/index.html +16 -0
  26. skeleton/infrastructure/bootstrap/src/templates/app/frontend/src/main.tsx +15 -0
  27. skeleton/infrastructure/bootstrap/src/templates/app/frontend/src/static/css/styles.css +28 -0
  28. skeleton/infrastructure/bootstrap/src/templates/app/frontend/src/static/img/skeleton.png +0 -0
  29. skeleton/infrastructure/bootstrap/src/templates/app/frontend/src/static/js/script.js +1 -0
  30. skeleton/infrastructure/bootstrap/src/templates/app/frontend/tsconfig.json +90 -0
  31. skeleton/infrastructure/bootstrap/src/templates/config/.gitignore +21 -0
  32. skeleton/infrastructure/bootstrap/src/templates/config/skeleton-dependencies.json +21 -0
  33. skeleton/infrastructure/bootstrap/src/templates/config/skeleton-layout.json +11 -0
  34. skeleton/infrastructure/bootstrap/src/templates/config/skeleton-schemas.json +20 -0
  35. skeleton/infrastructure/bootstrap/src/templates/config/skeleton-servers.json +16 -0
  36. skeleton/infrastructure/common/__init__.py +1 -0
  37. skeleton/infrastructure/common/config.json +8 -0
  38. skeleton/infrastructure/common/src/__init__.py +0 -0
  39. skeleton/infrastructure/common/src/config.py +37 -0
  40. skeleton/infrastructure/common/src/constants.py +4 -0
  41. skeleton/infrastructure/common/src/context.py +15 -0
  42. skeleton/infrastructure/common/src/env.py +12 -0
  43. skeleton/infrastructure/common/src/layout.py +68 -0
  44. skeleton/infrastructure/common/src/loader.py +8 -0
  45. skeleton/infrastructure/common/src/repository.py +12 -0
  46. skeleton/infrastructure/common/src/utils.py +26 -0
  47. skeleton/infrastructure/gateway/__init__.py +0 -0
  48. skeleton/infrastructure/gateway/settings/proxy.json +24 -0
  49. skeleton/infrastructure/gateway/settings/services.json +10 -0
  50. skeleton/infrastructure/gateway/src/__init__.py +0 -0
  51. skeleton/infrastructure/gateway/src/app.py +98 -0
  52. skeleton/infrastructure/gateway/src/client.py +68 -0
  53. skeleton/infrastructure/gateway/src/config.py +29 -0
  54. skeleton/infrastructure/gateway/src/context.py +31 -0
  55. skeleton/infrastructure/gateway/src/headers.py +17 -0
  56. skeleton/infrastructure/gateway/src/lifespan.py +17 -0
  57. skeleton/infrastructure/gateway/src/loader.py +113 -0
  58. skeleton/infrastructure/gateway/src/logging.py +37 -0
  59. skeleton/infrastructure/gateway/src/main.py +139 -0
  60. skeleton/infrastructure/gateway/src/parser.py +59 -0
  61. skeleton/infrastructure/gateway/src/registry.py +30 -0
  62. skeleton/infrastructure/gateway/src/reverseproxy.py +54 -0
  63. skeleton/infrastructure/gateway/src/router.py +74 -0
  64. skeleton/infrastructure/gateway/src/schemas.py +21 -0
  65. skeleton/infrastructure/gateway/src/service.py +123 -0
  66. skeleton/infrastructure/gateway/src/utils.py +48 -0
  67. skeleton/infrastructure/gateway/src/validation.py +89 -0
  68. skeleton/infrastructure/layout.json +12 -0
  69. skeleton/infrastructure/runners/__init__.py +0 -0
  70. skeleton/infrastructure/runners/src/__init__.py +0 -0
  71. skeleton/infrastructure/runners/src/actions.py +4 -0
  72. skeleton/infrastructure/runners/src/browser.py +18 -0
  73. skeleton/infrastructure/runners/src/cmd.py +7 -0
  74. skeleton/infrastructure/runners/src/command.py +7 -0
  75. skeleton/infrastructure/runners/src/frontend.py +35 -0
  76. skeleton/infrastructure/runners/src/gateway.py +104 -0
  77. skeleton/infrastructure/runners/src/hook.py +53 -0
  78. skeleton/infrastructure/runners/src/main.py +78 -0
  79. skeleton/infrastructure/runners/src/packages.py +35 -0
  80. skeleton/infrastructure/runners/src/parser.py +84 -0
  81. skeleton/infrastructure/runners/src/process.py +99 -0
  82. skeleton/infrastructure/runners/src/services.py +16 -0
  83. skeleton/infrastructure/runners/src/utils.py +6 -0
  84. skeleton/infrastructure/runners/src/validation.py +28 -0
  85. skeleton/packages/.skeleton/skeleton-cli-1.0.0.tgz +0 -0
  86. skeleton/packages/.skeleton/skeleton-webpack-1.0.0.tgz +0 -0
  87. skeleton/root.py +234 -0
  88. skeleton_framework-0.1.0.dist-info/METADATA +440 -0
  89. skeleton_framework-0.1.0.dist-info/RECORD +93 -0
  90. skeleton_framework-0.1.0.dist-info/WHEEL +5 -0
  91. skeleton_framework-0.1.0.dist-info/entry_points.txt +2 -0
  92. skeleton_framework-0.1.0.dist-info/licenses/LICENSE +146 -0
  93. skeleton_framework-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+
3
+ from pathlib import Path
4
+ from asyncio.subprocess import Process
5
+
6
+ from skeleton.infrastructure.gateway.src.context import ServiceContext
7
+
8
+ # actual runtime services!
9
+ class Service:
10
+
11
+ def __init__(self, config: ServiceContext):
12
+
13
+ self.config = config
14
+ self.proc: Process | None = None
15
+
16
+ self.watch_task: asyncio.Task | None = None
17
+
18
+ # avoid race conditions & multiple restarts
19
+ self.restart_lock = asyncio.Lock()
20
+
21
+ async def run(self) -> None:
22
+ await self.start()
23
+ self.watch_task = asyncio.create_task(self.watch_loop())
24
+
25
+ async def start(self) -> None:
26
+
27
+ print(
28
+ f"INFO: Starting {self.config.name}"
29
+ f" at http://{self.config.network.host}:"
30
+ f"{self.config.network.port}"
31
+ )
32
+
33
+ self.proc = await asyncio.create_subprocess_exec(
34
+ self.config.process.exe, "-m", "uvicorn",
35
+ f"{self.config.process.module}:{self.config.name}",
36
+ "--host", f"{self.config.network.host}",
37
+ "--port", f"{self.config.network.port}",
38
+ cwd=self.config.process.cwd,
39
+ )
40
+
41
+ # This loop runs forever ...
42
+ async def watch_loop(self) -> None:
43
+
44
+ print(
45
+ f"INFO {self.config.name}"
46
+ f" hot-module reload {self.config.watch.enabled}"
47
+ )
48
+
49
+ if not self.config.watch.enabled:
50
+ return
51
+
52
+ # lazy import awatch!
53
+ from watchfiles import awatch # type: ignore
54
+
55
+ try:
56
+ async for changes in awatch(self.config.watch.directory):
57
+ # check whether changes contains a file having extension
58
+ # that corresponds to any of the extensions in the config list!
59
+ if any(
60
+ Path(path).suffix
61
+ in self.config.watch.extensions
62
+ for _, path in changes
63
+ ):
64
+ await asyncio.sleep(0.25)
65
+ await self.restart()
66
+
67
+ except asyncio.CancelledError:
68
+ raise
69
+
70
+ # proper cleanup needed!
71
+ async def stop(self) -> None:
72
+ # check: does subprocess exist? + still running? (return code None)
73
+ if self.proc is not None and self.proc.returncode is None:
74
+ print(
75
+ f"INFO: Terminating "
76
+ f"{self.config.name} at "
77
+ f"{self.config.network.host} on "
78
+ f"{self.config.network.port}"
79
+ )
80
+ self.proc.terminate()
81
+ await self.proc.wait()
82
+
83
+ # context manager in restart() is equivalent but safer:
84
+ #
85
+ # await self.restart_lock.acquire()
86
+ #
87
+ # try:
88
+ # await self.stop()
89
+ # await self.start()
90
+ #
91
+ # finally:
92
+ # self.restart_lock.release()
93
+ #
94
+ # # async with context manager is safer
95
+ async def restart(self) -> None:
96
+ # async mutex lock => prevent multiple restarts!
97
+ # ONLY ONE coroutine may enter this block at any instance!
98
+ async with self.restart_lock:
99
+ print(
100
+ f"INFO: Restarting "
101
+ f"{self.config.name} at "
102
+ f"{self.config.network.host} on "
103
+ f"{self.config.network.port}"
104
+ )
105
+ await self.stop()
106
+ await self.start()
107
+
108
+ async def shutdown(self):
109
+
110
+ # cancel watcher tasks FIRST!
111
+ if self.watch_task:
112
+
113
+ self.watch_task.cancel()
114
+
115
+ try:
116
+ await self.watch_task
117
+ except asyncio.CancelledError:
118
+ pass
119
+
120
+ # THEN terminate subprocess!
121
+ await self.stop()
122
+ print("INFO: Graceful shutdown complete!")
123
+
@@ -0,0 +1,48 @@
1
+ from pathlib import Path
2
+
3
+ from skeleton.infrastructure.common.src.context import RuntimeContext
4
+
5
+ def build_static_dir(context: RuntimeContext) -> Path:
6
+
7
+ frontend = context.layout.frontend
8
+ lib = context.config.frontend["lib"]
9
+
10
+ build_dir = (
11
+ lib["src"]
12
+ if context.mode == "dev"
13
+ else lib["dist"]
14
+ )
15
+
16
+ return frontend/build_dir/lib["static"]
17
+
18
+ def normalize_extensions(
19
+ ext: tuple[str, ...]
20
+ ) -> tuple[str, ...]:
21
+
22
+ # store all normalized extensions!
23
+ normalized_ext: list[str] = []
24
+
25
+ for item in ext:
26
+
27
+ if not isinstance(item, str):
28
+ raise TypeError(f"Extension {item} must be a string")
29
+
30
+ # remove any leading/trailing whitespace
31
+ item = item.strip()
32
+
33
+ # check that extension is not a path
34
+ if "/" in item or "\\" in item:
35
+ raise ValueError(f"Invalid extension {item}")
36
+
37
+ # check that extension is a proper file type
38
+ if item == ".":
39
+ raise ValueError(f"'.' is not a valid extension/file type")
40
+
41
+ # if the item does NOT start with . then pre-pend it!
42
+ if not item.startswith("."):
43
+ item = f".{item}"
44
+
45
+ # if all is OK!
46
+ normalized_ext.append(item)
47
+
48
+ return tuple(normalized_ext)
@@ -0,0 +1,89 @@
1
+ from pathlib import Path
2
+
3
+ from skeleton.infrastructure.gateway.src.config import ServiceConfig
4
+ from skeleton.infrastructure.gateway.src.context import ServiceContext
5
+
6
+ from skeleton.infrastructure.common.src.utils import (
7
+ check_file_exists,
8
+ module_to_path,
9
+ )
10
+
11
+ class ConfigValidationError(Exception):
12
+ pass
13
+
14
+ def validate_service_configs(
15
+ services: list[ServiceConfig],
16
+ ) -> bool:
17
+
18
+ used_ports: set[int] = set()
19
+
20
+ for service in services:
21
+ validate_service_root(service.root)
22
+
23
+ validate_service_module(
24
+ service.process.module,
25
+ service.root,
26
+ )
27
+
28
+ validate_service_port(
29
+ service.network.port,
30
+ used_ports,
31
+ )
32
+
33
+ return True
34
+
35
+
36
+ def validate_service_contexts(
37
+ services: list[ServiceContext],
38
+ ) -> bool:
39
+
40
+ for service in services:
41
+ validate_service_executable(service.process.exe)
42
+ validate_service_watch_dir(service.watch.directory)
43
+ validate_service_cwd(service.process.cwd)
44
+
45
+ return True
46
+
47
+ def validate_service_root(root: Path) -> None:
48
+ if not root.exists():
49
+ raise FileNotFoundError(
50
+ f"service root does not exist: {root}"
51
+ )
52
+
53
+
54
+ def validate_service_module(
55
+ module: str,
56
+ root: Path,
57
+ ) -> None:
58
+
59
+ path = root / module_to_path(module)
60
+
61
+ check_file_exists("module", path)
62
+
63
+
64
+ def validate_service_port(
65
+ port: int,
66
+ used_ports: set[int],
67
+ ) -> None:
68
+
69
+ if port in used_ports:
70
+ raise ConfigValidationError(
71
+ f"Duplicate port detected: {port}"
72
+ )
73
+
74
+ used_ports.add(port)
75
+
76
+ def validate_service_executable(exe: Path) -> None:
77
+ check_file_exists("exe", exe)
78
+
79
+ def validate_service_cwd(cwd: Path) -> None:
80
+ if not cwd.exists():
81
+ raise FileNotFoundError(
82
+ f"cwd does not exist: {cwd}"
83
+ )
84
+
85
+ def validate_service_watch_dir(watch_dir: Path) -> None:
86
+ if not watch_dir.exists():
87
+ raise FileNotFoundError(
88
+ f"watch directory does not exist: {watch_dir}"
89
+ )
@@ -0,0 +1,12 @@
1
+ {
2
+ "layout": {
3
+
4
+ "infrastructure": "skeleton/infrastructure",
5
+
6
+ "common": "skeleton/infrastructure/common",
7
+ "gateway": "skeleton/infrastructure/gateway",
8
+ "runners": "skeleton/infrastructure/runners",
9
+
10
+ "packages": "skeleton/packages"
11
+ }
12
+ }
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ ACTIONS = {
2
+ "--start": "start",
3
+ "--build": "build",
4
+ }
@@ -0,0 +1,18 @@
1
+ import webbrowser
2
+
3
+ class BrowserOpener():
4
+ def open(self, url: str):
5
+ raise NotImplementedError(
6
+ f"No browser opener implemented!"
7
+ )
8
+
9
+ class SystemBrowserOpener(BrowserOpener):
10
+ def open(self, url: str) -> None:
11
+ webbrowser.open(url)
12
+
13
+ def resolve_browser(
14
+ choice: str
15
+ ) -> BrowserOpener | None:
16
+ if choice == "system":
17
+ return SystemBrowserOpener()
18
+ return None
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass(frozen=True, kw_only=True)
4
+ class CMD:
5
+ action: str
6
+ targets: list[str]
7
+ options: dict[str, str | bool]
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ @dataclass(frozen=True, kw_only=True)
5
+ class Command:
6
+ args: list[str]
7
+ cwd: Path | None = None
@@ -0,0 +1,35 @@
1
+ from skeleton.infrastructure.runners.src.command import Command
2
+ from skeleton.infrastructure.common.src.context import RuntimeContext
3
+ from skeleton.infrastructure.runners.src.utils import resolve_cmd
4
+
5
+ def start(
6
+ context: RuntimeContext,
7
+ flags: list[str] | None = None
8
+ ) -> Command:
9
+
10
+ flags = flags or []
11
+
12
+ return Command(
13
+ args=[
14
+ resolve_cmd("npm"),
15
+ "run", "serve",
16
+ *flags
17
+ ],
18
+ cwd=context.root / context.layout.frontend
19
+ )
20
+
21
+ def build(
22
+ context: RuntimeContext,
23
+ flags: list[str] | None = None
24
+ ) -> Command:
25
+
26
+ flags = flags or []
27
+
28
+ return Command(
29
+ args=[
30
+ resolve_cmd("npm"),
31
+ "run", "build",
32
+ *flags
33
+ ],
34
+ cwd=context.root / context.layout.frontend
35
+ )
@@ -0,0 +1,104 @@
1
+ from skeleton.infrastructure.common.src.constants import Bootstrap
2
+ from skeleton.infrastructure.common.src.context import RuntimeContext
3
+
4
+ from skeleton.infrastructure.runners.src.utils import resolve_cmd
5
+ from skeleton.infrastructure.runners.src.command import Command
6
+
7
+ from skeleton.infrastructure.common.src.utils import (
8
+ resolve_executable_path
9
+ )
10
+
11
+ # def start(
12
+ # context: RuntimeContext,
13
+ # flags: list[str] | None = None
14
+ # ) -> Command:
15
+
16
+ # flags = flags or []
17
+
18
+ # module = Bootstrap.GATEWAY_MODULE
19
+
20
+ # framework_root = context.layout.infrastructure.parent.parent
21
+
22
+ # python_exe = resolve_executable_path(
23
+ # context.layout.gateway
24
+ # )
25
+
26
+ # args = [
27
+ # str(python_exe),
28
+ # "-m", module,
29
+ # "--root", str(context.root),
30
+ # "--mode", str(context.mode),
31
+ # ]
32
+
33
+ # if context.options["watch"]:
34
+ # args.append("--watch")
35
+
36
+ # if context.options["open"]:
37
+ # args.append("--open")
38
+
39
+ # return Command(
40
+ # args=[*args, *flags],
41
+ # cwd=framework_root,
42
+ # )
43
+
44
+
45
+ import sys
46
+ from pathlib import Path
47
+
48
+ from skeleton.infrastructure.common.src.constants import Bootstrap
49
+ from skeleton.infrastructure.common.src.context import RuntimeContext
50
+ from skeleton.infrastructure.runners.src.utils import resolve_cmd
51
+ from skeleton.infrastructure.runners.src.command import Command
52
+ from skeleton.infrastructure.common.src.utils import resolve_executable_path
53
+
54
+
55
+ def start(
56
+ context: RuntimeContext,
57
+ flags: list[str] | None = None,
58
+ ) -> Command:
59
+
60
+ flags = flags or []
61
+
62
+ module = Bootstrap.GATEWAY_MODULE
63
+ framework_root = context.layout.infrastructure.parent.parent
64
+
65
+ # Source-repository development uses its dedicated gateway environment.
66
+ # An installed framework uses the Python environment containing the package.
67
+ if (framework_root / "run.py").is_file():
68
+ python_exe = resolve_executable_path(context.layout.gateway)
69
+ else:
70
+ python_exe = Path(sys.executable)
71
+
72
+ args = [
73
+ str(python_exe),
74
+ "-m", module,
75
+ "--root", str(context.root),
76
+ "--mode", str(context.mode),
77
+ ]
78
+
79
+ if context.options["watch"]:
80
+ args.append("--watch")
81
+
82
+ if context.options["open"]:
83
+ args.append("--open")
84
+
85
+ return Command(
86
+ args=[*args, *flags],
87
+ cwd=framework_root,
88
+ )
89
+
90
+ def build(
91
+ context: RuntimeContext,
92
+ flags: list[str] | None = None
93
+ ) -> Command:
94
+
95
+ flags = flags or []
96
+
97
+ return Command(
98
+ args=[
99
+ resolve_cmd("npm"),
100
+ "echo \"Error: no build specified\" && exit 1",
101
+ *flags
102
+ ],
103
+ cwd=context.root
104
+ )
@@ -0,0 +1,53 @@
1
+ import time, threading
2
+
3
+ from skeleton.infrastructure.common.src.context import RuntimeContext
4
+ from skeleton.infrastructure.runners.src.browser import resolve_browser
5
+
6
+ def create_dev_portal(context: RuntimeContext):
7
+
8
+ delay = 2.0
9
+ servers = context.config.servers["servers"]
10
+
11
+ gateway = servers["gateway"]
12
+ backend = servers["backend"]
13
+
14
+ gateway_host = gateway["host"]
15
+ gateway_port = gateway["port"]
16
+ gateway_url = f"http://{gateway_host}:{gateway_port}"
17
+
18
+ # for opening backend documentation
19
+ # valid options include "docs"/"redoc"
20
+ docs = context.config.services["services"]["backend"]["docs"]
21
+
22
+ backend_host = backend["host"]
23
+ backend_port = backend["port"]
24
+ backend_url = f"http://{backend_host}:{backend_port}/{docs}"
25
+
26
+ opener = resolve_browser("system")
27
+
28
+ urls = [
29
+ gateway_url,
30
+ backend_url,
31
+ ]
32
+
33
+ # self-object as first arg
34
+ def hook(manager):
35
+
36
+ if opener is None:
37
+ return
38
+
39
+ def _open():
40
+ time.sleep(delay)
41
+
42
+ for url in urls:
43
+ opener.open(url)
44
+
45
+ # When ctrl + C for shutdown:
46
+ # Exception ignored on threading shutdown:
47
+ # Traceback (most recent call last):
48
+ # File "C:\Users\Lambda\AppData\Local\Programs\Python\Python313\Lib\threading.py", line 1540, in _shutdown
49
+ # _thread_shutdown()
50
+ threading.Thread(target=_open, daemon=True).start()
51
+
52
+ return hook
53
+
@@ -0,0 +1,78 @@
1
+ from pathlib import Path
2
+
3
+ from skeleton.infrastructure.runners.src.cmd import CMD
4
+ from skeleton.infrastructure.runners.src.process import ProcessManager
5
+
6
+ from skeleton.infrastructure.common.src.constants import Bootstrap
7
+ from skeleton.infrastructure.common.src.context import RuntimeContext
8
+
9
+ from skeleton.infrastructure.common.src.config import load_runtime_config
10
+ from skeleton.infrastructure.common.src.layout import build_layout
11
+ from skeleton.infrastructure.common.src.env import build_env
12
+
13
+ from skeleton.infrastructure.runners.src.parser import parse
14
+
15
+ from skeleton.infrastructure.runners.src.services import SERVICES
16
+ from skeleton.infrastructure.runners.src.services import DEFAULT_TARGETS
17
+
18
+ def run(
19
+ framework_root: Path,
20
+ project_root: Path,
21
+ argv: list[str],
22
+ ):
23
+
24
+ raw = parse(argv)
25
+
26
+ raw["options"]["root"] = str(project_root)
27
+
28
+ if not raw["targets"]:
29
+ raw["targets"] = list(DEFAULT_TARGETS.copy())
30
+
31
+ cmd = CMD(**raw)
32
+
33
+ MODE = str(cmd.options.get("mode", "dev"))
34
+
35
+ env = build_env(project_root, MODE)
36
+
37
+ layout = build_layout(
38
+ project_root / Bootstrap.LAYOUT,
39
+ project_root=project_root,
40
+ framework_root=framework_root,
41
+ )
42
+
43
+ config = load_runtime_config(
44
+ layout.common / Bootstrap.CONFIG_FILE_EXT,
45
+ project_root,
46
+ framework_root,
47
+ layout,
48
+ )
49
+
50
+ context = RuntimeContext(
51
+ root=project_root,
52
+ mode=MODE,
53
+ args=argv,
54
+ env=env,
55
+ layout=layout,
56
+ config=config,
57
+ options=cmd.options,
58
+ )
59
+
60
+ manager = ProcessManager(context)
61
+
62
+ if context.options.get("open") and context.mode == "dev":
63
+
64
+ from skeleton.infrastructure.runners.src.hook import (
65
+ create_dev_portal
66
+ )
67
+
68
+ manager.register_hook(
69
+ create_dev_portal(context),
70
+ "after_start"
71
+ )
72
+
73
+ for target in cmd.targets:
74
+ service = SERVICES[target]
75
+ handler = getattr(service, cmd.action)
76
+ manager.register(handler(context))
77
+
78
+ manager.start()
@@ -0,0 +1,35 @@
1
+ from skeleton.infrastructure.runners.src.command import Command
2
+ from skeleton.infrastructure.common.src.context import RuntimeContext
3
+ from skeleton.infrastructure.runners.src.utils import resolve_cmd
4
+
5
+ def start(
6
+ context: RuntimeContext,
7
+ flags: list[str] | None = None
8
+ ) -> Command:
9
+
10
+ flags = flags or []
11
+
12
+ return Command(
13
+ args=[
14
+ resolve_cmd("npm"),
15
+ "run", "packages:watch",
16
+ *flags
17
+ ],
18
+ cwd=context.root
19
+ )
20
+
21
+ def build(
22
+ context: RuntimeContext,
23
+ flags: list[str] | None = None
24
+ ) -> Command:
25
+
26
+ flags = flags or []
27
+
28
+ return Command(
29
+ args=[
30
+ resolve_cmd("npm"),
31
+ "run", "packages:build",
32
+ *flags
33
+ ],
34
+ cwd=context.root
35
+ )