@mriekki/lean 0.0.0-stage → 0.1.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 (82) hide show
  1. package/README.md +67 -3
  2. package/assets/components.json +33 -0
  3. package/assets/core/distribution.json +51 -0
  4. package/assets/deployment/distribution.json +34 -0
  5. package/assets/generated/ASSEMBLY.json +382 -0
  6. package/assets/generated/core/core/agents/coach.agent.md +69 -0
  7. package/assets/generated/core/core/agents/reviewer.agent.md +85 -0
  8. package/assets/generated/core/core/instructions/lean-docs.instructions.md +14 -0
  9. package/assets/generated/core/core/instructions/lean-workflow.instructions.md +35 -0
  10. package/assets/generated/core/core/learning/CODING_DRILLS.md +35 -0
  11. package/assets/generated/core/core/learning/DIAGRAM_GUIDE.md +19 -0
  12. package/assets/generated/core/core/learning/ENGINEERING_LEARNING_WORKBOOK.md +82 -0
  13. package/assets/generated/core/core/learning/LEARNING_MAP.md +51 -0
  14. package/assets/generated/core/core/learning/LEARNING_PROGRESS.md +31 -0
  15. package/assets/generated/core/core/learning/RESOURCE_CATALOG.md +25 -0
  16. package/assets/generated/core/core/learning/SQL_DRILLS.md +27 -0
  17. package/assets/generated/core/core/learning/SYSTEM_DESIGN_DRILLS.md +21 -0
  18. package/assets/generated/core/core/process/COACHING_GUIDE.md +57 -0
  19. package/assets/generated/core/core/process/GUIDED_NEXT_STEPS.md +48 -0
  20. package/assets/generated/core/core/process/HUMAN_GATES.md +81 -0
  21. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +62 -0
  22. package/assets/generated/core/core/process/PRODUCTION_ENABLEMENT.md +59 -0
  23. package/assets/generated/core/core/process/PR_WORKFLOW.md +53 -0
  24. package/assets/generated/core/core/process/SETUP_ACCEPTANCE_CHECKLIST.md +72 -0
  25. package/assets/generated/core/core/process/WORK_ITEM_MODEL.md +56 -0
  26. package/assets/generated/core/core/skills/cleanup-branches/SKILL.md +190 -0
  27. package/assets/generated/core/core/skills/coach/SKILL.md +136 -0
  28. package/assets/generated/core/core/skills/code-walkthrough/SKILL.md +197 -0
  29. package/assets/generated/core/core/skills/core-sync/SKILL.md +220 -0
  30. package/assets/generated/core/core/skills/define-feature/SKILL.md +144 -0
  31. package/assets/generated/core/core/skills/deliver/SKILL.md +315 -0
  32. package/assets/generated/core/core/skills/deploy/SKILL.md +193 -0
  33. package/assets/generated/core/core/skills/grill-me/SKILL.md +77 -0
  34. package/assets/generated/core/core/skills/grill-with-docs/SKILL.md +90 -0
  35. package/assets/generated/core/core/skills/learning-review/SKILL.md +90 -0
  36. package/assets/generated/core/core/skills/next-step/SKILL.md +166 -0
  37. package/assets/generated/core/core/skills/production-readiness/SKILL.md +357 -0
  38. package/assets/generated/core/core/skills/pull-latest/SKILL.md +166 -0
  39. package/assets/generated/core/core/skills/refine-requirements/SKILL.md +207 -0
  40. package/assets/generated/core/core/skills/start-work/SKILL.md +178 -0
  41. package/assets/generated/core/core/skills/tear-down/SKILL.md +138 -0
  42. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +206 -0
  43. package/assets/generated/core/core/skills/wait-what/SKILL.md +86 -0
  44. package/assets/generated/core/core/templates/FEATURE_BRIEF_TEMPLATE.md +27 -0
  45. package/assets/generated/core/core/templates/PRODUCT_BRIEF_TEMPLATE.md +27 -0
  46. package/assets/generated/core/core/templates/PULL_REQUEST_TEMPLATE.md +37 -0
  47. package/assets/generated/core/core/templates/REQUIREMENTS_TEMPLATE.md +17 -0
  48. package/assets/generated/core/profiles/greenfield/lean-workspace.json +12 -0
  49. package/assets/generated/core/tools/package/check_baseline.py +102 -0
  50. package/assets/generated/core/tools/package/check_upstream.py +171 -0
  51. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -0
  52. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -0
  53. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -0
  54. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -0
  55. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -0
  56. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +416 -0
  57. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -0
  58. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -0
  59. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -0
  60. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -0
  61. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -0
  62. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -0
  63. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -0
  64. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -0
  65. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -0
  66. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -0
  67. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -0
  68. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -0
  69. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +335 -0
  70. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +340 -0
  71. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +86 -0
  72. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -0
  73. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -0
  74. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -0
  75. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -0
  76. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -0
  77. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -0
  78. package/bin/lean.mjs +9 -0
  79. package/package.json +33 -6
  80. package/src/cli.mjs +131 -0
  81. package/src/engine.mjs +243 -0
  82. package/src/package-data.mjs +18 -0
@@ -0,0 +1,437 @@
1
+ """Command-line interface for the Deployment Kit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+ from .box_platform import (
11
+ HOST_PLATFORM_WRITE_PHRASE,
12
+ apply_platform_change,
13
+ plan_platform_change,
14
+ platform_doctor,
15
+ platform_status,
16
+ )
17
+ from .doctor import target_doctor, workstation_doctor
18
+ from .errors import DeploymentKitError
19
+ from .keys import add_key_to_agent, agent_status, create_dedicated_key, public_key_info
20
+ from .profiles import load_profile, save_profile, validate_profile
21
+ from .route_registry import (
22
+ ROUTE_REMOVE_PHRASE,
23
+ ROUTE_WRITE_PHRASE,
24
+ apply_route_change,
25
+ plan_route_registration,
26
+ plan_route_removal,
27
+ route_inventory,
28
+ )
29
+ from .ssh import verify_access
30
+ from .trust import (
31
+ apply_trust_plan,
32
+ candidate_view,
33
+ plan_console_verified_trust,
34
+ scan_host_keys,
35
+ trust_plan_view,
36
+ )
37
+
38
+
39
+ LOCAL_WRITE_PHRASE = "[LOCAL CONFIG WRITE]"
40
+ KEY_WRITE_PHRASE = "[LOCAL KEY WRITE]"
41
+ AGENT_WRITE_PHRASE = "[SSH AGENT WRITE]"
42
+ TRUST_WRITE_PHRASE = "[LOCAL TRUST WRITE]"
43
+ TRUST_ROTATE_PHRASE = "[LOCAL TRUST ROTATION]"
44
+
45
+
46
+ def _print(value: Any, *, as_json: bool) -> None:
47
+ if as_json:
48
+ print(json.dumps(value, indent=2, sort_keys=True))
49
+ return
50
+ if isinstance(value, dict):
51
+ for key, item in value.items():
52
+ if isinstance(item, (dict, list)):
53
+ print(f"{key}: {json.dumps(item, sort_keys=True)}")
54
+ else:
55
+ print(f"{key}: {item}")
56
+ elif isinstance(value, list):
57
+ for item in value:
58
+ print(json.dumps(item, sort_keys=True))
59
+ else:
60
+ print(value)
61
+
62
+
63
+ def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
64
+ if args.yes:
65
+ return
66
+ entered = input(f"Type {phrase} to {action}: ").strip()
67
+ if entered != phrase:
68
+ raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
69
+
70
+
71
+ def _configure(args: argparse.Namespace) -> int:
72
+ profile = {
73
+ "schemaVersion": 1,
74
+ "targetName": args.name,
75
+ "targetOs": args.target_os,
76
+ "hostName": args.host_name,
77
+ "sshAlias": args.ssh_alias,
78
+ "user": args.user,
79
+ "port": args.port,
80
+ "identityFile": args.identity_file,
81
+ "knownHostsFile": args.known_hosts_file,
82
+ "addressOverride": args.address_override,
83
+ }
84
+ print(json.dumps(profile, indent=2))
85
+ _confirm(args, LOCAL_WRITE_PHRASE, "write this user-scoped target profile")
86
+ path = save_profile(profile)
87
+ print(f"Wrote target profile: {path}")
88
+ return 0
89
+
90
+
91
+ def _set_address(args: argparse.Namespace) -> int:
92
+ profile = load_profile(args.name)
93
+ proposed = dict(profile)
94
+ proposed["addressOverride"] = args.address
95
+ proposed = validate_profile(proposed, expected_target_name=args.name)
96
+ evidence = verify_access(proposed)
97
+ _print(
98
+ {
99
+ "targetName": args.name,
100
+ "currentOverride": profile["addressOverride"],
101
+ "proposedOverride": proposed["addressOverride"],
102
+ "strictVerification": evidence,
103
+ },
104
+ as_json=False,
105
+ )
106
+ _confirm(args, LOCAL_WRITE_PHRASE, "store this separately verified address override")
107
+ save_profile(proposed)
108
+ print(f"Stored address override for {args.name}: {proposed['addressOverride']}")
109
+ return 0
110
+
111
+
112
+ def _clear_address(args: argparse.Namespace) -> int:
113
+ profile = load_profile(args.name)
114
+ if profile["addressOverride"] is None:
115
+ print(f"No address override is set for {args.name}.")
116
+ return 0
117
+ _print(
118
+ {
119
+ "targetName": args.name,
120
+ "currentOverride": profile["addressOverride"],
121
+ "proposedOverride": None,
122
+ },
123
+ as_json=False,
124
+ )
125
+ _confirm(args, LOCAL_WRITE_PHRASE, "clear this target address override")
126
+ profile["addressOverride"] = None
127
+ save_profile(profile)
128
+ print(f"Cleared address override for {args.name}.")
129
+ return 0
130
+
131
+
132
+ def _platform_change(args: argparse.Namespace) -> int:
133
+ plan = plan_platform_change(args.name, args.command)
134
+ _print(plan, as_json=False)
135
+ _confirm(
136
+ args,
137
+ HOST_PLATFORM_WRITE_PHRASE,
138
+ f"{args.command} the shared host platform on target {args.name}",
139
+ )
140
+ _print(apply_platform_change(plan), as_json=args.json)
141
+ return 0
142
+
143
+
144
+ def _route_change(args: argparse.Namespace) -> int:
145
+ if args.command == "register":
146
+ plan = plan_route_registration(
147
+ args.name,
148
+ owner_repo=args.owner_repo,
149
+ environment=args.environment,
150
+ app_slug=args.app_slug,
151
+ upstream_container=args.upstream_container,
152
+ upstream_port=args.upstream_port,
153
+ hostnames=args.hostname,
154
+ )
155
+ phrase = ROUTE_WRITE_PHRASE
156
+ action = f"register/update route {plan['route']['routeId']} on target {args.name}"
157
+ else:
158
+ plan = plan_route_removal(
159
+ args.name,
160
+ owner_repo=args.owner_repo,
161
+ environment=args.environment,
162
+ app_slug=args.app_slug,
163
+ )
164
+ phrase = ROUTE_REMOVE_PHRASE
165
+ action = f"remove route {plan['route']['routeId']} from target {args.name}"
166
+ _print(plan, as_json=False)
167
+ _confirm(args, phrase, action)
168
+ _print(apply_route_change(plan), as_json=args.json)
169
+ return 0
170
+
171
+
172
+ def build_parser() -> argparse.ArgumentParser:
173
+ parser = argparse.ArgumentParser(prog="lean-poc-deploy")
174
+ subparsers = parser.add_subparsers(dest="group", required=True)
175
+
176
+ workstation = subparsers.add_parser("workstation", help="Workstation setup/readiness operations")
177
+ workstation_sub = workstation.add_subparsers(dest="command", required=True)
178
+ ws_doctor = workstation_sub.add_parser("doctor", help="Inspect workstation readiness without modifying it")
179
+ ws_doctor.add_argument("--json", action="store_true")
180
+
181
+ target = subparsers.add_parser("target", help="Logical deployment target operations")
182
+ target_sub = target.add_subparsers(dest="command", required=True)
183
+
184
+ target_doctor_parser = target_sub.add_parser("doctor", help="Inspect one target without modifying it")
185
+ target_doctor_parser.add_argument("name")
186
+ target_doctor_parser.add_argument("--json", action="store_true")
187
+
188
+ show = target_sub.add_parser("show", help="Show normalized non-secret target profile")
189
+ show.add_argument("name")
190
+ show.add_argument("--json", action="store_true")
191
+
192
+ verify = target_sub.add_parser("verify", help="Verify strict public-key SSH access without remote writes")
193
+ verify.add_argument("name")
194
+ verify.add_argument("--json", action="store_true")
195
+
196
+ configure = target_sub.add_parser("configure", help="Write a user-scoped target profile")
197
+ configure.add_argument("name")
198
+ configure.add_argument("--target-os", default="ubuntu")
199
+ configure.add_argument("--host-name", required=True)
200
+ configure.add_argument("--ssh-alias", required=True)
201
+ configure.add_argument("--user", required=True)
202
+ configure.add_argument("--port", type=int, default=22)
203
+ configure.add_argument("--identity-file", required=True)
204
+ configure.add_argument("--known-hosts-file", required=True)
205
+ configure.add_argument("--address-override")
206
+ configure.add_argument("--yes", action="store_true", help="Accept the bounded local profile write")
207
+
208
+ key_create = target_sub.add_parser(
209
+ "key-create",
210
+ help="Create the configured dedicated ed25519 key without exposing its passphrase to this tool",
211
+ )
212
+ key_create.add_argument("name")
213
+ key_create.add_argument("--yes", action="store_true")
214
+
215
+ key_public = target_sub.add_parser(
216
+ "key-public",
217
+ help="Show the dedicated public key and fingerprint for console-assisted installation",
218
+ )
219
+ key_public.add_argument("name")
220
+ key_public.add_argument("--json", action="store_true")
221
+
222
+ agent_check = target_sub.add_parser("agent-status", help="Inspect whether the dedicated key is loaded")
223
+ agent_check.add_argument("name")
224
+ agent_check.add_argument("--json", action="store_true")
225
+
226
+ agent_add = target_sub.add_parser(
227
+ "agent-add",
228
+ help="Load the configured dedicated key into an already-running SSH agent",
229
+ )
230
+ agent_add.add_argument("name")
231
+ agent_add.add_argument("--yes", action="store_true")
232
+
233
+ trust_candidates = target_sub.add_parser(
234
+ "trust-candidates",
235
+ help="Read candidate host-key fingerprints; candidates are never trusted by discovery",
236
+ )
237
+ trust_candidates.add_argument("name")
238
+ trust_candidates.add_argument(
239
+ "--address",
240
+ help="Exact current IP independently obtained from the trusted target console",
241
+ )
242
+ trust_candidates.add_argument("--json", action="store_true")
243
+
244
+ trust_enroll = target_sub.add_parser(
245
+ "trust-enroll",
246
+ help="Enroll one host key only when it matches the independently observed console fingerprint",
247
+ )
248
+ trust_enroll.add_argument("name")
249
+ trust_enroll.add_argument("--fingerprint", required=True)
250
+ trust_enroll.add_argument("--address")
251
+ trust_enroll.add_argument("--yes", action="store_true")
252
+
253
+ trust_rotate = target_sub.add_parser(
254
+ "trust-rotate",
255
+ help="Explicitly replace existing target trust after independent console verification",
256
+ )
257
+ trust_rotate.add_argument("name")
258
+ trust_rotate.add_argument("--fingerprint", required=True)
259
+ trust_rotate.add_argument("--address")
260
+ trust_rotate.add_argument("--yes", action="store_true")
261
+
262
+ address_set = target_sub.add_parser(
263
+ "address-set",
264
+ help="Store an IP override only after strict SSH verification against existing target identity",
265
+ )
266
+ address_set.add_argument("name")
267
+ address_set.add_argument("--address", required=True)
268
+ address_set.add_argument("--yes", action="store_true")
269
+
270
+ address_clear = target_sub.add_parser("address-clear", help="Clear a target's temporary address override")
271
+ address_clear.add_argument("name")
272
+ address_clear.add_argument("--yes", action="store_true")
273
+
274
+ route = subparsers.add_parser(
275
+ "route",
276
+ help="Owned application route operations on a ready shared host platform",
277
+ )
278
+ route_sub = route.add_subparsers(dest="command", required=True)
279
+ route_inventory_parser = route_sub.add_parser(
280
+ "inventory", help="Read managed route ownership plus legacy route filenames without writes"
281
+ )
282
+ route_inventory_parser.add_argument("name")
283
+ route_inventory_parser.add_argument("--json", action="store_true")
284
+
285
+ route_register = route_sub.add_parser(
286
+ "register", help="Create or update one POC/environment-owned Caddy + mDNS route"
287
+ )
288
+ route_register.add_argument("name")
289
+ route_register.add_argument("--owner-repo", required=True)
290
+ route_register.add_argument("--environment", required=True)
291
+ route_register.add_argument("--app-slug", required=True)
292
+ route_register.add_argument("--upstream-container", required=True)
293
+ route_register.add_argument("--upstream-port", type=int, required=True)
294
+ route_register.add_argument("--hostname", action="append", required=True)
295
+ route_register.add_argument("--json", action="store_true")
296
+ route_register.add_argument("--yes", action="store_true")
297
+
298
+ route_remove = route_sub.add_parser(
299
+ "remove", help="Remove only one owned route/alias registration, preserving app runtime and data"
300
+ )
301
+ route_remove.add_argument("name")
302
+ route_remove.add_argument("--owner-repo", required=True)
303
+ route_remove.add_argument("--environment", required=True)
304
+ route_remove.add_argument("--app-slug", required=True)
305
+ route_remove.add_argument("--json", action="store_true")
306
+ route_remove.add_argument("--yes", action="store_true")
307
+
308
+ platform = subparsers.add_parser(
309
+ "platform",
310
+ help="Shared Ubuntu box platform operations; never registers or removes application routes",
311
+ )
312
+ platform_sub = platform.add_subparsers(dest="command", required=True)
313
+ platform_status_parser = platform_sub.add_parser(
314
+ "status", help="Read shared host-platform state without remote writes"
315
+ )
316
+ platform_status_parser.add_argument("name")
317
+ platform_status_parser.add_argument("--json", action="store_true")
318
+ platform_doctor_parser = platform_sub.add_parser(
319
+ "doctor", help="Read shared host-platform readiness and next action"
320
+ )
321
+ platform_doctor_parser.add_argument("name")
322
+ platform_doctor_parser.add_argument("--json", action="store_true")
323
+ for command in ("install", "upgrade"):
324
+ change = platform_sub.add_parser(
325
+ command,
326
+ help=(
327
+ "Install shared host platform on a fresh target"
328
+ if command == "install"
329
+ else "Upgrade an already Kit-owned shared host platform"
330
+ ),
331
+ )
332
+ change.add_argument("name")
333
+ change.add_argument("--json", action="store_true")
334
+ change.add_argument("--yes", action="store_true")
335
+
336
+ return parser
337
+
338
+
339
+ def main(argv: list[str] | None = None) -> int:
340
+ args = build_parser().parse_args(argv)
341
+ try:
342
+ if args.group == "workstation" and args.command == "doctor":
343
+ _print(workstation_doctor(), as_json=args.json)
344
+ return 0
345
+ if args.group == "target" and args.command == "doctor":
346
+ _print(target_doctor(args.name), as_json=args.json)
347
+ return 0
348
+ if args.group == "target" and args.command == "show":
349
+ _print(load_profile(args.name), as_json=args.json)
350
+ return 0
351
+ if args.group == "target" and args.command == "verify":
352
+ _print(verify_access(load_profile(args.name)), as_json=args.json)
353
+ return 0
354
+ if args.group == "target" and args.command == "configure":
355
+ return _configure(args)
356
+ if args.group == "target" and args.command == "key-create":
357
+ profile = load_profile(args.name)
358
+ _print(
359
+ {
360
+ "targetName": args.name,
361
+ "identityFile": profile["identityFile"],
362
+ "keyType": "ed25519",
363
+ "passphraseHandling": "entered directly into ssh-keygen",
364
+ },
365
+ as_json=False,
366
+ )
367
+ _confirm(args, KEY_WRITE_PHRASE, "create this dedicated workstation key")
368
+ _print(create_dedicated_key(profile), as_json=False)
369
+ return 0
370
+ if args.group == "target" and args.command == "key-public":
371
+ _print(public_key_info(load_profile(args.name)), as_json=args.json)
372
+ return 0
373
+ if args.group == "target" and args.command == "agent-status":
374
+ _print(agent_status(load_profile(args.name)), as_json=args.json)
375
+ return 0
376
+ if args.group == "target" and args.command == "agent-add":
377
+ profile = load_profile(args.name)
378
+ status = agent_status(profile)
379
+ _print(status, as_json=False)
380
+ if status["loaded"]:
381
+ return 0
382
+ if status["state"] == "agent-unavailable":
383
+ raise DeploymentKitError(
384
+ "The SSH agent is unavailable. Start/enable it as a separate workstation action; "
385
+ "this command will not change agent service configuration."
386
+ )
387
+ _confirm(args, AGENT_WRITE_PHRASE, "load this dedicated key into the running SSH agent")
388
+ _print(add_key_to_agent(profile), as_json=False)
389
+ return 0
390
+ if args.group == "target" and args.command == "trust-candidates":
391
+ candidates = scan_host_keys(load_profile(args.name), console_address=args.address)
392
+ _print([candidate_view(item) for item in candidates], as_json=args.json)
393
+ return 0
394
+ if args.group == "target" and args.command in {"trust-enroll", "trust-rotate"}:
395
+ profile = load_profile(args.name)
396
+ rotate = args.command == "trust-rotate"
397
+ plan = plan_console_verified_trust(
398
+ profile,
399
+ fingerprint=args.fingerprint,
400
+ rotate=rotate,
401
+ console_address=args.address,
402
+ )
403
+ _print(trust_plan_view(plan), as_json=False)
404
+ phrase = TRUST_ROTATE_PHRASE if rotate else TRUST_WRITE_PHRASE
405
+ action = (
406
+ "replace existing target trust with this console-verified host key"
407
+ if rotate
408
+ else "enroll this console-verified target host key"
409
+ )
410
+ _confirm(args, phrase, action)
411
+ _print(apply_trust_plan(profile, plan), as_json=False)
412
+ return 0
413
+ if args.group == "target" and args.command == "address-set":
414
+ return _set_address(args)
415
+ if args.group == "target" and args.command == "address-clear":
416
+ return _clear_address(args)
417
+ if args.group == "route" and args.command == "inventory":
418
+ _print(route_inventory(args.name), as_json=args.json)
419
+ return 0
420
+ if args.group == "route" and args.command in {"register", "remove"}:
421
+ return _route_change(args)
422
+ if args.group == "platform" and args.command == "status":
423
+ _print(platform_status(args.name), as_json=args.json)
424
+ return 0
425
+ if args.group == "platform" and args.command == "doctor":
426
+ _print(platform_doctor(args.name), as_json=args.json)
427
+ return 0
428
+ if args.group == "platform" and args.command in {"install", "upgrade"}:
429
+ return _platform_change(args)
430
+ raise DeploymentKitError("Unsupported command.")
431
+ except DeploymentKitError as error:
432
+ print(f"ERROR: {error}", file=sys.stderr)
433
+ return 2
434
+
435
+
436
+ if __name__ == "__main__":
437
+ raise SystemExit(main())
@@ -0,0 +1,121 @@
1
+ """Read-only workstation and target readiness checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .errors import DeploymentKitError
10
+ from .profiles import config_root, load_profile, target_profile_path
11
+ from .security import check_ssh_material_security, validate_target_trust_file
12
+ from .ssh import resolve_addresses
13
+
14
+
15
+ def workstation_doctor(*, home: Path | None = None) -> dict[str, Any]:
16
+ root = config_root(home)
17
+ targets_dir = root / "targets"
18
+ profiles = []
19
+ if targets_dir.is_dir() and not targets_dir.is_symlink():
20
+ profiles = sorted(path.stem for path in targets_dir.glob("*.json") if path.is_file() and not path.is_symlink())
21
+
22
+ tools = {name: shutil.which(name) for name in ("ssh", "ssh-keygen", "ssh-keyscan", "ssh-add")}
23
+ missing_tools = [name for name, path in tools.items() if path is None]
24
+
25
+ if not root.exists() and not profiles:
26
+ state = "fresh-workstation"
27
+ elif missing_tools:
28
+ state = "blocked"
29
+ elif profiles:
30
+ state = "configured"
31
+ else:
32
+ state = "partially-configured"
33
+
34
+ return {
35
+ "state": state,
36
+ "configRoot": str(root),
37
+ "profiles": profiles,
38
+ "tools": tools,
39
+ "missingTools": missing_tools,
40
+ "mutated": False,
41
+ }
42
+
43
+
44
+ def target_doctor(name: str, *, home: Path | None = None) -> dict[str, Any]:
45
+ path = target_profile_path(name, home)
46
+ if not path.exists():
47
+ return {
48
+ "targetName": name,
49
+ "state": "needs-profile",
50
+ "profilePath": str(path),
51
+ "nextAction": "configure-target-profile",
52
+ "mutated": False,
53
+ }
54
+
55
+ try:
56
+ profile = load_profile(name, home=home)
57
+ except DeploymentKitError as error:
58
+ return {
59
+ "targetName": name,
60
+ "state": "blocked",
61
+ "profilePath": str(path),
62
+ "reason": str(error),
63
+ "mutated": False,
64
+ }
65
+
66
+ identity = Path(profile["identityFile"])
67
+ known_hosts = Path(profile["knownHostsFile"])
68
+ if not identity.is_file():
69
+ return {
70
+ "targetName": name,
71
+ "state": "needs-key",
72
+ "profilePath": str(path),
73
+ "identityFile": str(identity),
74
+ "nextAction": "create-or-select-dedicated-key",
75
+ "mutated": False,
76
+ }
77
+ if not known_hosts.is_file() or known_hosts.stat().st_size == 0:
78
+ return {
79
+ "targetName": name,
80
+ "state": "needs-trust",
81
+ "profilePath": str(path),
82
+ "knownHostsFile": str(known_hosts),
83
+ "nextAction": "verify-console-fingerprint-before-trust-enrollment",
84
+ "mutated": False,
85
+ }
86
+
87
+ try:
88
+ check_ssh_material_security(identity, known_hosts)
89
+ validate_target_trust_file(profile)
90
+ except DeploymentKitError as error:
91
+ return {
92
+ "targetName": name,
93
+ "state": "blocked",
94
+ "profilePath": str(path),
95
+ "reason": str(error),
96
+ "nextAction": "repair-ssh-material-or-trust",
97
+ "mutated": False,
98
+ }
99
+
100
+ try:
101
+ addresses = resolve_addresses(profile)
102
+ except DeploymentKitError as error:
103
+ return {
104
+ "targetName": name,
105
+ "state": "needs-address",
106
+ "profilePath": str(path),
107
+ "hostName": profile["hostName"],
108
+ "reason": str(error),
109
+ "nextAction": "set-separately-verified-address-override",
110
+ "mutated": False,
111
+ }
112
+
113
+ return {
114
+ "targetName": name,
115
+ "state": "ready-to-verify",
116
+ "profilePath": str(path),
117
+ "hostName": profile["hostName"],
118
+ "addresses": addresses,
119
+ "addressSource": "override" if profile["addressOverride"] else "dns-mdns",
120
+ "mutated": False,
121
+ }
@@ -0,0 +1,25 @@
1
+ """Top-level console entrypoint for Deployment Kit command groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .app_cli import main as app_main
8
+ from .cli import main as core_main
9
+ from .sqlite_cli import main as sqlite_main
10
+ from .teardown_cli import main as teardown_main
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ args = list(sys.argv[1:] if argv is None else argv)
15
+ if args and args[0] == "app":
16
+ return app_main(args[1:])
17
+ if args and args[0] == "sqlite":
18
+ return sqlite_main(args[1:])
19
+ if args and args[0] == "tear-down":
20
+ return teardown_main(args[1:])
21
+ if not args or args == ["--help"] or args == ["-h"]:
22
+ print("Command group: app Generic immutable application runtime inventory/deployment")
23
+ print("Command group: sqlite SQLite persistence import/backup/migration/rollback checks")
24
+ print("Command group: tear-down Scoped destructive POC environment/local cleanup")
25
+ return core_main(args)
@@ -0,0 +1,5 @@
1
+ """Deployment Kit exception types."""
2
+
3
+
4
+ class DeploymentKitError(Exception):
5
+ """Expected fail-closed operational or configuration error."""