@mriekki/lean 0.1.0 → 0.2.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 (41) hide show
  1. package/README.md +71 -67
  2. package/assets/components.json +33 -33
  3. package/assets/core/distribution.json +52 -51
  4. package/assets/deployment/distribution.json +34 -34
  5. package/assets/generated/ASSEMBLY.json +38 -33
  6. package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
  7. package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
  8. package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
  9. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
  10. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
  11. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
  12. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
  13. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
  14. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
  15. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
  16. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
  17. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
  18. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
  19. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
  20. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
  21. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
  22. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
  23. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
  24. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
  25. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
  26. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
  27. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
  28. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
  29. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
  30. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
  31. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
  32. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
  33. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
  34. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
  35. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
  36. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
  37. package/bin/lean.mjs +9 -9
  38. package/package.json +33 -33
  39. package/src/cli.mjs +131 -131
  40. package/src/engine.mjs +243 -243
  41. package/src/package-data.mjs +18 -18
@@ -1,204 +1,204 @@
1
- """User-scoped logical target profiles."""
2
-
3
- from __future__ import annotations
4
-
5
- import ipaddress
6
- import json
7
- import os
8
- import re
9
- from pathlib import Path
10
- from typing import Any
11
-
12
- from .errors import DeploymentKitError
13
-
14
-
15
- PROFILE_SCHEMA_VERSION = 1
16
- TARGET_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
17
- ALIAS_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
18
- USER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]{0,31}$")
19
- HOST_LABEL_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
20
- SUPPORTED_TARGET_OS = {"ubuntu"}
21
-
22
-
23
- def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
24
- value: dict[str, Any] = {}
25
- for key, item in pairs:
26
- if key in value:
27
- raise DeploymentKitError(f"Duplicate JSON property: {key}")
28
- value[key] = item
29
- return value
30
-
31
-
32
- def config_root(home: Path | None = None) -> Path:
33
- override = os.environ.get("LEAN_POC_HOME")
34
- if override:
35
- candidate = Path(override).expanduser()
36
- if not candidate.is_absolute():
37
- raise DeploymentKitError("LEAN_POC_HOME must be an absolute path.")
38
- return candidate.resolve()
39
- return ((home or Path.home()).resolve() / ".lean-poc").resolve()
40
-
41
-
42
- def validate_target_name(name: str) -> str:
43
- if not isinstance(name, str) or TARGET_NAME_RE.fullmatch(name) is None:
44
- raise DeploymentKitError(
45
- "Target name must use lowercase letters, digits, and hyphens, and must start with a letter or digit."
46
- )
47
- return name
48
-
49
-
50
- def target_profile_path(name: str, home: Path | None = None) -> Path:
51
- validate_target_name(name)
52
- return config_root(home) / "targets" / f"{name}.json"
53
-
54
-
55
- def _validate_host_name(value: Any) -> str:
56
- if not isinstance(value, str) or not value or len(value) > 253:
57
- raise DeploymentKitError("hostName must be a DNS/mDNS hostname.")
58
- if value.endswith("."):
59
- value = value[:-1]
60
- try:
61
- ipaddress.ip_address(value)
62
- except ValueError:
63
- pass
64
- else:
65
- raise DeploymentKitError(
66
- "hostName must be a stable DNS/mDNS name, not an IP address; use addressOverride for a verified temporary IP."
67
- )
68
- labels = value.split(".")
69
- if any(HOST_LABEL_RE.fullmatch(label) is None for label in labels):
70
- raise DeploymentKitError("hostName must be a valid DNS/mDNS hostname.")
71
- return value.lower()
72
-
73
-
74
- def _ssh_path(value: Any, field: str, home: Path) -> Path:
75
- if not isinstance(value, str) or not value.strip():
76
- raise DeploymentKitError(f"{field} must not be empty.")
77
- if value == "~":
78
- candidate = home
79
- elif value.startswith("~/") or value.startswith("~\\"):
80
- candidate = home / value[2:]
81
- else:
82
- candidate = Path(value)
83
- if not candidate.is_absolute():
84
- raise DeploymentKitError(f"{field} must be absolute or home-relative with '~'.")
85
- resolved = candidate.resolve()
86
- ssh_dir = (home / ".ssh").resolve()
87
- if resolved == ssh_dir or not resolved.is_relative_to(ssh_dir):
88
- raise DeploymentKitError(f"{field} must be a file inside the user's SSH directory: {ssh_dir}")
89
- return resolved
90
-
91
-
92
- def validate_profile(
93
- value: dict[str, Any],
94
- *,
95
- expected_target_name: str | None = None,
96
- home: Path | None = None,
97
- ) -> dict[str, Any]:
98
- expected_fields = {
99
- "schemaVersion",
100
- "targetName",
101
- "targetOs",
102
- "hostName",
103
- "sshAlias",
104
- "user",
105
- "port",
106
- "identityFile",
107
- "knownHostsFile",
108
- "addressOverride",
109
- }
110
- if not isinstance(value, dict) or set(value) != expected_fields:
111
- raise DeploymentKitError(
112
- "Target profile must contain exactly: " + ", ".join(sorted(expected_fields)) + "."
113
- )
114
- if type(value["schemaVersion"]) is not int or value["schemaVersion"] != PROFILE_SCHEMA_VERSION:
115
- raise DeploymentKitError("Unsupported target profile schemaVersion.")
116
-
117
- target_name = validate_target_name(value["targetName"])
118
- if expected_target_name is not None and target_name != expected_target_name:
119
- raise DeploymentKitError(
120
- f"Target profile mismatch: expected '{expected_target_name}', got '{target_name}'."
121
- )
122
-
123
- target_os = value["targetOs"]
124
- if target_os not in SUPPORTED_TARGET_OS:
125
- raise DeploymentKitError(
126
- f"Unsupported targetOs '{target_os}'. Initial support is limited to: {', '.join(sorted(SUPPORTED_TARGET_OS))}."
127
- )
128
-
129
- host_name = _validate_host_name(value["hostName"])
130
- alias = value["sshAlias"]
131
- if not isinstance(alias, str) or ALIAS_RE.fullmatch(alias) is None:
132
- raise DeploymentKitError("sshAlias contains unsupported characters.")
133
-
134
- username = value["user"]
135
- if not isinstance(username, str) or USER_RE.fullmatch(username) is None:
136
- raise DeploymentKitError("user contains unsupported characters.")
137
-
138
- port = value["port"]
139
- if type(port) is not int or not 1 <= port <= 65535:
140
- raise DeploymentKitError("port must be an integer from 1 through 65535.")
141
-
142
- user_home = (home or Path.home()).resolve()
143
- identity_file = _ssh_path(value["identityFile"], "identityFile", user_home)
144
- known_hosts_file = _ssh_path(value["knownHostsFile"], "knownHostsFile", user_home)
145
- if identity_file == known_hosts_file:
146
- raise DeploymentKitError("identityFile and knownHostsFile must be different paths.")
147
-
148
- address_override = value["addressOverride"]
149
- if address_override is not None:
150
- if not isinstance(address_override, str):
151
- raise DeploymentKitError("addressOverride must be null or an IP address.")
152
- try:
153
- parsed = ipaddress.ip_address(address_override)
154
- except ValueError as error:
155
- raise DeploymentKitError("addressOverride must be a valid IPv4 or IPv6 address.") from error
156
- if isinstance(parsed, ipaddress.IPv6Address) and parsed.scope_id is not None:
157
- raise DeploymentKitError("addressOverride must not include an IPv6 scope identifier.")
158
- address_override = str(parsed)
159
-
160
- return {
161
- "schemaVersion": PROFILE_SCHEMA_VERSION,
162
- "targetName": target_name,
163
- "targetOs": target_os,
164
- "hostName": host_name,
165
- "sshAlias": alias,
166
- "user": username,
167
- "port": port,
168
- "identityFile": str(identity_file),
169
- "knownHostsFile": str(known_hosts_file),
170
- "addressOverride": address_override,
171
- }
172
-
173
-
174
- def load_profile(name: str, *, home: Path | None = None) -> dict[str, Any]:
175
- path = target_profile_path(name, home)
176
- if path.is_symlink() or path.parent.is_symlink():
177
- raise DeploymentKitError("Target profile and its containing directory must not be symbolic links.")
178
- if not path.is_file():
179
- raise DeploymentKitError(f"Target profile is missing: {path}")
180
- try:
181
- with path.open(encoding="utf-8") as source:
182
- value = json.load(source, object_pairs_hook=_unique_object)
183
- except (OSError, UnicodeError, json.JSONDecodeError) as error:
184
- raise DeploymentKitError(f"Cannot read target profile {path}: {error}") from error
185
- return validate_profile(value, expected_target_name=name, home=home)
186
-
187
-
188
- def save_profile(value: dict[str, Any], *, home: Path | None = None) -> Path:
189
- normalized = validate_profile(value, home=home)
190
- path = target_profile_path(normalized["targetName"], home)
191
- directory = path.parent
192
- if directory.exists() and (directory.is_symlink() or not directory.is_dir()):
193
- raise DeploymentKitError(f"Target profile directory is not a real directory: {directory}")
194
- directory.mkdir(parents=True, exist_ok=True, mode=0o700)
195
- if os.name != "nt":
196
- directory.chmod(0o700)
197
- if path.exists() and path.is_symlink():
198
- raise DeploymentKitError(f"Refusing to replace symbolic-link target profile: {path}")
199
- temporary = path.with_suffix(".tmp")
200
- temporary.write_text(json.dumps(normalized, indent=2) + "\n", encoding="utf-8")
201
- if os.name != "nt":
202
- temporary.chmod(0o600)
203
- temporary.replace(path)
204
- return path
1
+ """User-scoped logical target profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import json
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .errors import DeploymentKitError
13
+
14
+
15
+ PROFILE_SCHEMA_VERSION = 1
16
+ TARGET_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
17
+ ALIAS_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
18
+ USER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]{0,31}$")
19
+ HOST_LABEL_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
20
+ SUPPORTED_TARGET_OS = {"ubuntu"}
21
+
22
+
23
+ def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
24
+ value: dict[str, Any] = {}
25
+ for key, item in pairs:
26
+ if key in value:
27
+ raise DeploymentKitError(f"Duplicate JSON property: {key}")
28
+ value[key] = item
29
+ return value
30
+
31
+
32
+ def config_root(home: Path | None = None) -> Path:
33
+ override = os.environ.get("LEAN_POC_HOME")
34
+ if override:
35
+ candidate = Path(override).expanduser()
36
+ if not candidate.is_absolute():
37
+ raise DeploymentKitError("LEAN_POC_HOME must be an absolute path.")
38
+ return candidate.resolve()
39
+ return ((home or Path.home()).resolve() / ".lean-poc").resolve()
40
+
41
+
42
+ def validate_target_name(name: str) -> str:
43
+ if not isinstance(name, str) or TARGET_NAME_RE.fullmatch(name) is None:
44
+ raise DeploymentKitError(
45
+ "Target name must use lowercase letters, digits, and hyphens, and must start with a letter or digit."
46
+ )
47
+ return name
48
+
49
+
50
+ def target_profile_path(name: str, home: Path | None = None) -> Path:
51
+ validate_target_name(name)
52
+ return config_root(home) / "targets" / f"{name}.json"
53
+
54
+
55
+ def _validate_host_name(value: Any) -> str:
56
+ if not isinstance(value, str) or not value or len(value) > 253:
57
+ raise DeploymentKitError("hostName must be a DNS/mDNS hostname.")
58
+ if value.endswith("."):
59
+ value = value[:-1]
60
+ try:
61
+ ipaddress.ip_address(value)
62
+ except ValueError:
63
+ pass
64
+ else:
65
+ raise DeploymentKitError(
66
+ "hostName must be a stable DNS/mDNS name, not an IP address; use addressOverride for a verified temporary IP."
67
+ )
68
+ labels = value.split(".")
69
+ if any(HOST_LABEL_RE.fullmatch(label) is None for label in labels):
70
+ raise DeploymentKitError("hostName must be a valid DNS/mDNS hostname.")
71
+ return value.lower()
72
+
73
+
74
+ def _ssh_path(value: Any, field: str, home: Path) -> Path:
75
+ if not isinstance(value, str) or not value.strip():
76
+ raise DeploymentKitError(f"{field} must not be empty.")
77
+ if value == "~":
78
+ candidate = home
79
+ elif value.startswith("~/") or value.startswith("~\\"):
80
+ candidate = home / value[2:]
81
+ else:
82
+ candidate = Path(value)
83
+ if not candidate.is_absolute():
84
+ raise DeploymentKitError(f"{field} must be absolute or home-relative with '~'.")
85
+ resolved = candidate.resolve()
86
+ ssh_dir = (home / ".ssh").resolve()
87
+ if resolved == ssh_dir or not resolved.is_relative_to(ssh_dir):
88
+ raise DeploymentKitError(f"{field} must be a file inside the user's SSH directory: {ssh_dir}")
89
+ return resolved
90
+
91
+
92
+ def validate_profile(
93
+ value: dict[str, Any],
94
+ *,
95
+ expected_target_name: str | None = None,
96
+ home: Path | None = None,
97
+ ) -> dict[str, Any]:
98
+ expected_fields = {
99
+ "schemaVersion",
100
+ "targetName",
101
+ "targetOs",
102
+ "hostName",
103
+ "sshAlias",
104
+ "user",
105
+ "port",
106
+ "identityFile",
107
+ "knownHostsFile",
108
+ "addressOverride",
109
+ }
110
+ if not isinstance(value, dict) or set(value) != expected_fields:
111
+ raise DeploymentKitError(
112
+ "Target profile must contain exactly: " + ", ".join(sorted(expected_fields)) + "."
113
+ )
114
+ if type(value["schemaVersion"]) is not int or value["schemaVersion"] != PROFILE_SCHEMA_VERSION:
115
+ raise DeploymentKitError("Unsupported target profile schemaVersion.")
116
+
117
+ target_name = validate_target_name(value["targetName"])
118
+ if expected_target_name is not None and target_name != expected_target_name:
119
+ raise DeploymentKitError(
120
+ f"Target profile mismatch: expected '{expected_target_name}', got '{target_name}'."
121
+ )
122
+
123
+ target_os = value["targetOs"]
124
+ if target_os not in SUPPORTED_TARGET_OS:
125
+ raise DeploymentKitError(
126
+ f"Unsupported targetOs '{target_os}'. Initial support is limited to: {', '.join(sorted(SUPPORTED_TARGET_OS))}."
127
+ )
128
+
129
+ host_name = _validate_host_name(value["hostName"])
130
+ alias = value["sshAlias"]
131
+ if not isinstance(alias, str) or ALIAS_RE.fullmatch(alias) is None:
132
+ raise DeploymentKitError("sshAlias contains unsupported characters.")
133
+
134
+ username = value["user"]
135
+ if not isinstance(username, str) or USER_RE.fullmatch(username) is None:
136
+ raise DeploymentKitError("user contains unsupported characters.")
137
+
138
+ port = value["port"]
139
+ if type(port) is not int or not 1 <= port <= 65535:
140
+ raise DeploymentKitError("port must be an integer from 1 through 65535.")
141
+
142
+ user_home = (home or Path.home()).resolve()
143
+ identity_file = _ssh_path(value["identityFile"], "identityFile", user_home)
144
+ known_hosts_file = _ssh_path(value["knownHostsFile"], "knownHostsFile", user_home)
145
+ if identity_file == known_hosts_file:
146
+ raise DeploymentKitError("identityFile and knownHostsFile must be different paths.")
147
+
148
+ address_override = value["addressOverride"]
149
+ if address_override is not None:
150
+ if not isinstance(address_override, str):
151
+ raise DeploymentKitError("addressOverride must be null or an IP address.")
152
+ try:
153
+ parsed = ipaddress.ip_address(address_override)
154
+ except ValueError as error:
155
+ raise DeploymentKitError("addressOverride must be a valid IPv4 or IPv6 address.") from error
156
+ if isinstance(parsed, ipaddress.IPv6Address) and parsed.scope_id is not None:
157
+ raise DeploymentKitError("addressOverride must not include an IPv6 scope identifier.")
158
+ address_override = str(parsed)
159
+
160
+ return {
161
+ "schemaVersion": PROFILE_SCHEMA_VERSION,
162
+ "targetName": target_name,
163
+ "targetOs": target_os,
164
+ "hostName": host_name,
165
+ "sshAlias": alias,
166
+ "user": username,
167
+ "port": port,
168
+ "identityFile": str(identity_file),
169
+ "knownHostsFile": str(known_hosts_file),
170
+ "addressOverride": address_override,
171
+ }
172
+
173
+
174
+ def load_profile(name: str, *, home: Path | None = None) -> dict[str, Any]:
175
+ path = target_profile_path(name, home)
176
+ if path.is_symlink() or path.parent.is_symlink():
177
+ raise DeploymentKitError("Target profile and its containing directory must not be symbolic links.")
178
+ if not path.is_file():
179
+ raise DeploymentKitError(f"Target profile is missing: {path}")
180
+ try:
181
+ with path.open(encoding="utf-8") as source:
182
+ value = json.load(source, object_pairs_hook=_unique_object)
183
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
184
+ raise DeploymentKitError(f"Cannot read target profile {path}: {error}") from error
185
+ return validate_profile(value, expected_target_name=name, home=home)
186
+
187
+
188
+ def save_profile(value: dict[str, Any], *, home: Path | None = None) -> Path:
189
+ normalized = validate_profile(value, home=home)
190
+ path = target_profile_path(normalized["targetName"], home)
191
+ directory = path.parent
192
+ if directory.exists() and (directory.is_symlink() or not directory.is_dir()):
193
+ raise DeploymentKitError(f"Target profile directory is not a real directory: {directory}")
194
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
195
+ if os.name != "nt":
196
+ directory.chmod(0o700)
197
+ if path.exists() and path.is_symlink():
198
+ raise DeploymentKitError(f"Refusing to replace symbolic-link target profile: {path}")
199
+ temporary = path.with_suffix(".tmp")
200
+ temporary.write_text(json.dumps(normalized, indent=2) + "\n", encoding="utf-8")
201
+ if os.name != "nt":
202
+ temporary.chmod(0o600)
203
+ temporary.replace(path)
204
+ return path