@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,6 +1,6 @@
1
1
  ---
2
2
  name: upstream-fix
3
- description: Route reusable bugs discovered in a consumer to the canonical shared repository, allow bounded upstream implementation, and return adoption to exact baseline sync.
3
+ description: Route reusable bugs discovered in a consumer to the canonical shared repository, allow bounded upstream implementation, and return adoption through the consumer's actual provenance/update boundary.
4
4
  argument-hint: "[optional problem summary]"
5
5
  user-invocable: true
6
6
  disable-model-invocation: true
@@ -13,32 +13,26 @@ disable-model-invocation: true
13
13
  The governing rule is:
14
14
 
15
15
  ```text
16
- discover in the consumer
17
- → diagnose/prove in the consumer
18
- → fix in the owning canonical repository
16
+ discover in consumer
17
+ → diagnose/prove in consumer
18
+ → fix in owning canonical repository
19
19
  → deliver there
20
- → sync the accepted shared commit back into the consumer
21
- → verify again in the consumer
20
+ → adopt through the consumer's recorded distribution boundary
21
+ → verify again in consumer
22
22
  ```
23
23
 
24
- A temporary edit to a shared-managed file inside the consumer may be useful as diagnosis/proof, but it is never the canonical delivered fix.
24
+ A temporary edit to a shared-managed file inside a consumer may be useful as diagnosis/proof, but it is never the canonical delivered fix.
25
25
 
26
26
  ## 1. Invocation authorization
27
27
 
28
28
  Invoking `/upstream-fix` authorizes:
29
29
 
30
30
  - read-only ownership/provenance inspection in the consumer;
31
- - classification of the problem to the owning layer;
31
+ - classification to the smallest canonical owner;
32
32
  - inspection of already-available local canonical repository candidates;
33
- - when ownership and the intended patch are clear and a canonical checkout is safe, routine creation/use of one short-lived canonical work branch and application of only the bounded equivalent fix plus its focused tests/docs.
33
+ - when ownership and the bounded patch are clear and a canonical checkout is safe, routine creation/use of one short-lived canonical work branch and application of only that bounded fix plus focused tests/docs.
34
34
 
35
- Invocation does **not** authorize:
36
-
37
- - commit, push, PR creation, merge, release publication, or deployment;
38
- - rewriting/discarding consumer work;
39
- - changing more than one canonical repository in the same bounded fix;
40
- - broad filesystem scans or cloning shared repositories merely to satisfy the helper;
41
- - treating a temporary consumer patch as accepted shared implementation.
35
+ Invocation does **not** authorize commit, push, PR creation, merge, release publication, package publication, deployment, rewriting/discarding consumer work, or changing multiple canonical repositories in one bounded fix.
42
36
 
43
37
  Normal review and `/deliver` occur later while focused on the owning canonical repository.
44
38
 
@@ -46,20 +40,16 @@ Normal review and `/deliver` occur later while focused on the owning canonical r
46
40
 
47
41
  From the consumer root inspect, when present:
48
42
 
43
+ - `LEAN_BASELINE.json`;
49
44
  - `CORE_BASELINE.json`;
50
45
  - `DEPLOYMENT_BASELINE.json`;
51
46
  - `.github/lean-workspace.json`;
52
47
  - current Git branch/status/diff;
53
48
  - the files involved in the reported problem or temporary proof.
54
49
 
55
- Run repository-local managed-baseline checks when available:
56
-
57
- ```text
58
- python tools/lean-core/check-core-baseline.py --root .
59
- python tools/lean-deployment/check-deployment-baseline.py
60
- ```
50
+ Run repository-local managed-baseline checks when available.
61
51
 
62
- Do not assume every generated-looking file is shared-owned. Use baseline managed-path records as the primary ownership evidence.
52
+ Do not assume every generated-looking file is shared-owned. Use baseline managed-path records as the primary ownership evidence, and use `LEAN_BASELINE.json` to determine whether component baselines are managed together by an umbrella package.
63
53
 
64
54
  ## 3. Classify the owner
65
55
 
@@ -67,7 +57,7 @@ Use the smallest owning layer that would need the same fix for another consumer.
67
57
 
68
58
  ### Consumer-owned
69
59
 
70
- Examples: application domain logic, UI behavior, app-specific configuration, product tests, consumer architecture.
60
+ Application domain logic, UI behavior, app-specific configuration, product tests, and consumer architecture normally remain consumer-owned.
71
61
 
72
62
  Result:
73
63
 
@@ -76,13 +66,11 @@ CONSUMER FIX
76
66
  Owner: current repository
77
67
  ```
78
68
 
79
- Stop upstream routing and continue through the consumer's normal `/start-work` / implementation / review / `/deliver` flow.
69
+ Return to the consumer's normal `/start-work` / implementation / review / `/deliver` flow.
80
70
 
81
71
  ### Core-owned
82
72
 
83
- Evidence includes a changed/problematic path managed by `CORE_BASELINE.json`, or behavior defined by canonical Lean workflow skills/agents/instructions.
84
-
85
- Result:
73
+ Evidence includes a path managed by `CORE_BASELINE.json` or behavior defined by reusable Core workflow assets.
86
74
 
87
75
  ```text
88
76
  CORE BUG
@@ -91,26 +79,33 @@ Owner: <repository recorded by CORE_BASELINE.json>
91
79
 
92
80
  ### Deployment-Kit-owned
93
81
 
94
- Evidence includes a changed/problematic path managed by `DEPLOYMENT_BASELINE.json`, or reusable deployment behavior owned by the pinned Deployment Kit.
95
-
96
- Result:
82
+ Evidence includes a path managed by `DEPLOYMENT_BASELINE.json` or reusable deployment mechanics owned by that recorded Kit.
97
83
 
98
84
  ```text
99
85
  DEPLOYMENT BUG
100
86
  Owner: <repository recorded by DEPLOYMENT_BASELINE.json>
101
87
  ```
102
88
 
89
+ ### Package assembly / Lean Platform-owned
90
+
91
+ When canonical component source is correct but the published package bytes, component pins, package engine, or umbrella baseline behavior is wrong:
92
+
93
+ ```text
94
+ LEAN PLATFORM BUG
95
+ Owner: package/platform repository supported by evidence
96
+ ```
97
+
103
98
  ### Generator/control-plane owned
104
99
 
105
- If generated output is wrong but the managed payload itself is correct, classify the generator/orchestrator rather than patching the generated consumer. Resolve ownership from repository/project evidence; do not guess between Bootstrap and a control plane when evidence is insufficient.
100
+ If the package payload is correct but generation/publication/orchestration materializes it incorrectly, classify Bootstrap/control-plane ownership from evidence rather than patching a generated consumer.
106
101
 
107
- If ownership is genuinely ambiguous, stop at `UPSTREAM OWNER UNCLEAR` and present the competing evidence. Do not write to multiple repositories speculatively.
102
+ If ownership is genuinely ambiguous, stop at `UPSTREAM OWNER UNCLEAR`. Do not write to multiple repositories speculatively.
108
103
 
109
104
  ## 4. Temporary consumer proof
110
105
 
111
- A temporary consumer edit to a shared-managed path is allowed for diagnosis when it helps prove the fix in the real application context.
106
+ A temporary consumer edit to a shared-managed path is allowed for diagnosis when it proves the fix in real application context.
112
107
 
113
- Clearly report it as:
108
+ Report it explicitly:
114
109
 
115
110
  ```text
116
111
  TEMPORARY SHARED-MANAGED PROOF
@@ -119,48 +114,38 @@ Canonical owner: <repo>
119
114
  Do not deliver this consumer copy.
120
115
  ```
121
116
 
122
- Do not silently revert/discard the proof, because it may contain useful evidence. Do not stage/commit it as ordinary consumer implementation. Adoption later replaces the private proof with the accepted shared baseline.
117
+ Do not silently discard the proof and do not stage/commit it as ordinary consumer implementation.
123
118
 
124
- ## 5. Resolve a canonical local checkout
119
+ ## 5. Resolve a safe canonical checkout
125
120
 
126
- For the classified owner, consider local candidates in this order:
121
+ For the classified owner consider, in order:
127
122
 
128
- 1. explicit path supplied by the human;
129
- 2. an already-open workspace folder whose Git remote unambiguously matches the recorded canonical repository;
123
+ 1. an explicit path supplied by the human;
124
+ 2. an already-open workspace folder with the expected Git remote;
130
125
  3. a normal sibling checkout derived from the canonical repository name.
131
126
 
132
- Do not broadly scan the filesystem. A multi-root maintainer workspace is a convenience, not a requirement.
133
-
134
- A candidate is safe for bounded upstream implementation only when:
127
+ Do not broadly scan the filesystem and do not clone automatically merely to continue.
135
128
 
136
- - it is the expected Git repository;
137
- - Git state can be inspected;
138
- - no merge/rebase/cherry-pick is in progress;
139
- - unrelated local work is absent;
140
- - branch state does not require reset/rebase/force/stash/discard operations.
141
-
142
- If no safe local canonical checkout exists, report the exact canonical repository and the proven consumer evidence. Do not clone automatically just to continue.
129
+ A candidate is safe only when it is the expected repository, has no merge/rebase/cherry-pick in progress, has no unrelated local work, and does not require stash/reset/rebase/force/discard operations.
143
130
 
144
131
  ## 6. Port the bounded fix upstream
145
132
 
146
- When the safe local canonical checkout exists and the equivalent fix is clear:
147
-
148
- 1. inspect the canonical implementation and tests before editing;
149
- 2. create/use a repository-appropriate short-lived branch tied to the upstream issue/work item when needed;
150
- 3. reproduce the root cause against canonical code where practical;
151
- 4. apply the reusable fix in the canonical source, not by copying consumer-specific names/configuration;
152
- 5. add/update focused canonical tests that would have caught the defect;
153
- 6. update canonical docs only when the contract changed;
154
- 7. run relevant local checks when available;
155
- 8. show the canonical meaningful diff and observed validation.
133
+ When a safe canonical checkout exists and the equivalent reusable fix is clear:
156
134
 
157
- Preserve consumer specificity. A shared fix must not hard-code the consumer name, target, data, environment, route, or product requirement merely because that consumer exposed the bug.
135
+ 1. inspect canonical implementation/tests before editing;
136
+ 2. create/use one repository-appropriate short-lived branch;
137
+ 3. reproduce the root cause canonically where practical;
138
+ 4. apply the reusable fix without consumer-specific names/configuration;
139
+ 5. add/update focused tests that would have caught the defect;
140
+ 6. update canonical docs when the contract changed;
141
+ 7. run relevant local checks;
142
+ 8. show the meaningful canonical diff and observed validation.
158
143
 
159
- If the consumer proof cannot be generalized safely, stop and reclassify or ask for a narrower canonical design rather than forcing the patch upstream.
144
+ If the consumer proof cannot be generalized safely, stop and reclassify rather than forcing it upstream.
160
145
 
161
146
  ## 7. Handoff to canonical delivery
162
147
 
163
- After the upstream implementation is ready, report:
148
+ When the upstream implementation is ready:
164
149
 
165
150
  ```text
166
151
  UPSTREAM FIX READY
@@ -172,23 +157,53 @@ Validation: <observed checks>
172
157
  Next: focus the canonical repository and run /deliver
173
158
  ```
174
159
 
175
- `/upstream-fix` does not commit/push/open/merge the canonical PR itself. `/deliver` retains the normal reviewed-diff and merge/finalize gates in exactly one repository.
160
+ `/upstream-fix` does not commit/push/open/merge the canonical PR itself.
161
+
162
+ ## 8. Adopt the accepted fix through the right boundary
163
+
164
+ After the canonical PR is merged and required default-branch CI is green, re-read the consumer provenance before choosing adoption.
165
+
166
+ ### Package-managed consumer (`LEAN_BASELINE.json` present)
167
+
168
+ Do **not** direct-sync Core or Deployment Kit into the consumer.
169
+
170
+ The accepted component fix must first become part of an accepted published Lean package:
171
+
172
+ ```text
173
+ canonical Core/Deployment fix merged
174
+ → Lean Platform pins the exact accepted component commit
175
+ → Lean Platform PR + CI
176
+ → explicit package publication gate
177
+ → published exact @mriekki/lean version
178
+ → consumer /lean-update <version>
179
+ ```
180
+
181
+ Until that package exists, report:
182
+
183
+ ```text
184
+ PACKAGE RELEASE REQUIRED
185
+ Consumer adoption is blocked on a Lean package that contains the accepted upstream commit.
186
+ ```
187
+
188
+ Do not silently patch `LEAN_BASELINE.json`, `CORE_BASELINE.json`, or `DEPLOYMENT_BASELINE.json` to pretend the package contains the new component.
189
+
190
+ ### Component-only Core consumer (`LEAN_BASELINE.json` absent)
191
+
192
+ A Core-owned fix may return through `/core-sync` using the exact accepted Core commit/distribution.
176
193
 
177
- ## 8. Adopt the accepted fix back into the consumer
194
+ ### Component-only Deployment consumer (`LEAN_BASELINE.json` absent)
178
195
 
179
- After the canonical PR is merged and its accepted default-branch CI is green:
196
+ A Deployment-owned fix may return through the canonical Deployment Kit compatibility sync and refresh `DEPLOYMENT_BASELINE.json`.
180
197
 
181
- - Core-owned fixes return through `/core-sync` using the exact accepted Core commit/distribution;
182
- - Deployment-Kit-owned fixes return through the canonical Deployment Kit consumer-sync capability and refresh `DEPLOYMENT_BASELINE.json`;
183
- - generator/control-plane fixes are validated by regenerating or using that platform's canonical update path, not by manually editing generated managed files.
198
+ ### Generator/control-plane fix
184
199
 
185
- Then rerun the original consumer reproduction and baseline checks.
200
+ Validate through that platform's canonical regeneration/update path, never by manually editing generated managed files.
186
201
 
187
- A successful consumer retest is evidence that the shared fix works in context; it does not turn the consumer copy into the canonical source.
202
+ Then rerun the original reproduction and baseline checks in the consumer.
188
203
 
189
204
  ## 9. Consumer delivery guard
190
205
 
191
- If `/deliver` is invoked in a consumer with unexplained Core- or Deployment-Kit-managed file drift, stop before staging/commit/push. Route reusable managed-file changes through `/upstream-fix` and accepted baseline sync first.
206
+ If `/deliver` is invoked in a consumer with unexplained Core-, Deployment-, or Lean-package-managed drift, stop before staging/commit/push. Route reusable managed-file changes through `/upstream-fix` and then through the appropriate accepted update boundary.
192
207
 
193
208
  Consumer-owned changes may be delivered normally when managed baselines are clean.
194
209
 
@@ -197,10 +212,10 @@ Consumer-owned changes may be delivered normally when managed baselines are clea
197
212
  `/upstream-fix` never:
198
213
 
199
214
  - merges changes;
200
- - publishes releases/tags;
215
+ - publishes releases/tags/packages;
201
216
  - deploys production;
202
- - commits one change across multiple repositories;
217
+ - commits one bounded fix across multiple repositories;
203
218
  - auto-stashes, resets, rebases, force-pushes, or discards work;
204
- - turns a maintainer multi-root workspace into generated repository content;
205
219
  - requires ordinary consumer users to clone shared repositories;
206
- - treats a temporary shared-managed consumer edit as a legitimate permanent fork.
220
+ - treats a temporary managed consumer edit as a permanent fork;
221
+ - bypasses `LEAN_BASELINE.json` with component-only sync.
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- from lean_poc_deployment.entrypoint import main
5
-
6
-
7
- if __name__ == "__main__":
8
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ from lean_poc_deployment.entrypoint import main
5
+
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -1,3 +1,3 @@
1
- """Lean POC Deployment Kit."""
2
-
3
- __version__ = "0.1.0.dev0"
1
+ """Lean POC Deployment Kit."""
2
+
3
+ __version__ = "0.1.0.dev0"
@@ -1,3 +1,3 @@
1
- from .entrypoint import main
2
-
3
- raise SystemExit(main())
1
+ from .entrypoint import main
2
+
3
+ raise SystemExit(main())
@@ -1,140 +1,140 @@
1
- """CLI for generic immutable application runtime deployment."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import sys
8
- from typing import Any
9
-
10
- from .app_runtime import (
11
- APP_DEPLOY_WRITE_PHRASE,
12
- PRODUCTION_APP_DEPLOY_WRITE_PHRASE,
13
- app_inventory,
14
- apply_app_deploy,
15
- deploy_write_phrase,
16
- normalize_app_spec,
17
- plan_app_deploy,
18
- )
19
- from .errors import DeploymentKitError
20
- from .sqlite_adapter import assert_app_spec_matches_persistence
21
-
22
-
23
- def _print(value: Any, *, as_json: bool) -> None:
24
- if as_json:
25
- print(json.dumps(value, indent=2, sort_keys=True))
26
- return
27
- if isinstance(value, dict):
28
- for key, item in value.items():
29
- if isinstance(item, (dict, list)):
30
- print(f"{key}: {json.dumps(item, sort_keys=True)}")
31
- else:
32
- print(f"{key}: {item}")
33
- return
34
- print(value)
35
-
36
-
37
- def _parse_env(items: list[str]) -> list[tuple[str, str]]:
38
- result: list[tuple[str, str]] = []
39
- for item in items:
40
- if "=" not in item:
41
- raise DeploymentKitError("--env entries must use NAME=value form.")
42
- key, value = item.split("=", 1)
43
- result.append((key, value))
44
- return result
45
-
46
-
47
- def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
48
- if args.yes:
49
- return
50
- entered = input(f"Type {phrase} to {action}: ").strip()
51
- if entered != phrase:
52
- raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
53
-
54
-
55
- def build_parser() -> argparse.ArgumentParser:
56
- parser = argparse.ArgumentParser(prog="lean-poc-deploy app")
57
- sub = parser.add_subparsers(dest="command", required=True)
58
-
59
- inventory = sub.add_parser(
60
- "inventory",
61
- help="Read Kit-managed application runtime ownership/readiness without writes",
62
- )
63
- inventory.add_argument("name")
64
- inventory.add_argument("--json", action="store_true")
65
-
66
- deploy = sub.add_parser(
67
- "deploy",
68
- help="Deploy one immutable application runtime without modifying routes",
69
- )
70
- deploy.add_argument("name")
71
- deploy.add_argument("--owner-repo", required=True)
72
- deploy.add_argument("--environment", required=True)
73
- deploy.add_argument("--app-slug", required=True)
74
- deploy.add_argument("--container-name", required=True)
75
- deploy.add_argument("--image", required=True)
76
- deploy.add_argument("--runtime-port", type=int, required=True)
77
- deploy.add_argument("--health-path", required=True)
78
- deploy.add_argument("--run-as", required=True)
79
- deploy.add_argument(
80
- "--persistence-mode",
81
- choices=("none", "directory"),
82
- required=True,
83
- )
84
- deploy.add_argument("--data-container-path")
85
- deploy.add_argument(
86
- "--env",
87
- action="append",
88
- default=[],
89
- help="Non-secret runtime environment value in NAME=value form; repeat as needed",
90
- )
91
- deploy.add_argument("--json", action="store_true")
92
- deploy.add_argument("--yes", action="store_true")
93
- return parser
94
-
95
-
96
- def main(argv: list[str] | None = None) -> int:
97
- args = build_parser().parse_args(argv)
98
- try:
99
- if args.command == "inventory":
100
- _print(app_inventory(args.name), as_json=args.json)
101
- return 0
102
-
103
- spec = normalize_app_spec(
104
- owner_repo=args.owner_repo,
105
- environment=args.environment,
106
- app_slug=args.app_slug,
107
- container_name=args.container_name,
108
- image=args.image,
109
- runtime_port=args.runtime_port,
110
- health_path=args.health_path,
111
- run_as=args.run_as,
112
- persistence_mode=args.persistence_mode,
113
- data_container_path=args.data_container_path,
114
- environment_variables=_parse_env(args.env),
115
- )
116
- assert_app_spec_matches_persistence(args.name, spec)
117
- plan = plan_app_deploy(args.name, spec)
118
- _print(
119
- {key: value for key, value in plan.items() if key != "spec"},
120
- as_json=False,
121
- )
122
- phrase = deploy_write_phrase(plan)
123
- _confirm(
124
- args,
125
- phrase,
126
- f"deploy {plan['appSlug']} ({plan['environment']}) on target {args.name}",
127
- )
128
- _print(apply_app_deploy(plan), as_json=args.json)
129
- return 0
130
- except DeploymentKitError as error:
131
- print(f"ERROR: {error}", file=sys.stderr)
132
- return 2
133
-
134
-
135
- __all__ = [
136
- "APP_DEPLOY_WRITE_PHRASE",
137
- "PRODUCTION_APP_DEPLOY_WRITE_PHRASE",
138
- "build_parser",
139
- "main",
140
- ]
1
+ """CLI for generic immutable application runtime deployment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+ from .app_runtime import (
11
+ APP_DEPLOY_WRITE_PHRASE,
12
+ PRODUCTION_APP_DEPLOY_WRITE_PHRASE,
13
+ app_inventory,
14
+ apply_app_deploy,
15
+ deploy_write_phrase,
16
+ normalize_app_spec,
17
+ plan_app_deploy,
18
+ )
19
+ from .errors import DeploymentKitError
20
+ from .sqlite_adapter import assert_app_spec_matches_persistence
21
+
22
+
23
+ def _print(value: Any, *, as_json: bool) -> None:
24
+ if as_json:
25
+ print(json.dumps(value, indent=2, sort_keys=True))
26
+ return
27
+ if isinstance(value, dict):
28
+ for key, item in value.items():
29
+ if isinstance(item, (dict, list)):
30
+ print(f"{key}: {json.dumps(item, sort_keys=True)}")
31
+ else:
32
+ print(f"{key}: {item}")
33
+ return
34
+ print(value)
35
+
36
+
37
+ def _parse_env(items: list[str]) -> list[tuple[str, str]]:
38
+ result: list[tuple[str, str]] = []
39
+ for item in items:
40
+ if "=" not in item:
41
+ raise DeploymentKitError("--env entries must use NAME=value form.")
42
+ key, value = item.split("=", 1)
43
+ result.append((key, value))
44
+ return result
45
+
46
+
47
+ def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
48
+ if args.yes:
49
+ return
50
+ entered = input(f"Type {phrase} to {action}: ").strip()
51
+ if entered != phrase:
52
+ raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
53
+
54
+
55
+ def build_parser() -> argparse.ArgumentParser:
56
+ parser = argparse.ArgumentParser(prog="lean-poc-deploy app")
57
+ sub = parser.add_subparsers(dest="command", required=True)
58
+
59
+ inventory = sub.add_parser(
60
+ "inventory",
61
+ help="Read Kit-managed application runtime ownership/readiness without writes",
62
+ )
63
+ inventory.add_argument("name")
64
+ inventory.add_argument("--json", action="store_true")
65
+
66
+ deploy = sub.add_parser(
67
+ "deploy",
68
+ help="Deploy one immutable application runtime without modifying routes",
69
+ )
70
+ deploy.add_argument("name")
71
+ deploy.add_argument("--owner-repo", required=True)
72
+ deploy.add_argument("--environment", required=True)
73
+ deploy.add_argument("--app-slug", required=True)
74
+ deploy.add_argument("--container-name", required=True)
75
+ deploy.add_argument("--image", required=True)
76
+ deploy.add_argument("--runtime-port", type=int, required=True)
77
+ deploy.add_argument("--health-path", required=True)
78
+ deploy.add_argument("--run-as", required=True)
79
+ deploy.add_argument(
80
+ "--persistence-mode",
81
+ choices=("none", "directory"),
82
+ required=True,
83
+ )
84
+ deploy.add_argument("--data-container-path")
85
+ deploy.add_argument(
86
+ "--env",
87
+ action="append",
88
+ default=[],
89
+ help="Non-secret runtime environment value in NAME=value form; repeat as needed",
90
+ )
91
+ deploy.add_argument("--json", action="store_true")
92
+ deploy.add_argument("--yes", action="store_true")
93
+ return parser
94
+
95
+
96
+ def main(argv: list[str] | None = None) -> int:
97
+ args = build_parser().parse_args(argv)
98
+ try:
99
+ if args.command == "inventory":
100
+ _print(app_inventory(args.name), as_json=args.json)
101
+ return 0
102
+
103
+ spec = normalize_app_spec(
104
+ owner_repo=args.owner_repo,
105
+ environment=args.environment,
106
+ app_slug=args.app_slug,
107
+ container_name=args.container_name,
108
+ image=args.image,
109
+ runtime_port=args.runtime_port,
110
+ health_path=args.health_path,
111
+ run_as=args.run_as,
112
+ persistence_mode=args.persistence_mode,
113
+ data_container_path=args.data_container_path,
114
+ environment_variables=_parse_env(args.env),
115
+ )
116
+ assert_app_spec_matches_persistence(args.name, spec)
117
+ plan = plan_app_deploy(args.name, spec)
118
+ _print(
119
+ {key: value for key, value in plan.items() if key != "spec"},
120
+ as_json=False,
121
+ )
122
+ phrase = deploy_write_phrase(plan)
123
+ _confirm(
124
+ args,
125
+ phrase,
126
+ f"deploy {plan['appSlug']} ({plan['environment']}) on target {args.name}",
127
+ )
128
+ _print(apply_app_deploy(plan), as_json=args.json)
129
+ return 0
130
+ except DeploymentKitError as error:
131
+ print(f"ERROR: {error}", file=sys.stderr)
132
+ return 2
133
+
134
+
135
+ __all__ = [
136
+ "APP_DEPLOY_WRITE_PHRASE",
137
+ "PRODUCTION_APP_DEPLOY_WRITE_PHRASE",
138
+ "build_parser",
139
+ "main",
140
+ ]