@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,340 @@
1
+ """Remote Bash template for the SQLite persistence adapter."""
2
+
3
+ REMOTE_SCRIPT = r'''#!/usr/bin/env bash
4
+ set -Eeuo pipefail
5
+
6
+ ACTION="$1"
7
+ EXPECTED_RESERVATION_SHA="${2:-}"
8
+ DEPLOY_USER="${3:-}"
9
+ APP_ROOT="__APP_ROOT__"
10
+ SQLITE_SCHEMA_VERSION="__SQLITE_SCHEMA_VERSION__"
11
+ OWNER_B64="__OWNER_B64__"
12
+ ENV_B64="__ENV_B64__"
13
+ APP_B64="__APP_B64__"
14
+ IMAGE_B64="__IMAGE_B64__"
15
+ RUN_AS_B64="__RUN_AS_B64__"
16
+ DB_FILE_B64="__DB_FILE_B64__"
17
+ SOURCE_DB_B64="__SOURCE_DB_B64__"
18
+ FIRST_INSTALL="__FIRST_INSTALL__"
19
+ RESERVATION_SHA="__RESERVATION_SHA__"
20
+ ENV_KEYS=(__ENV_KEYS__)
21
+ ENV_VALUES=(__ENV_VALUES__)
22
+
23
+ b64d() { printf '%s' "$1" | base64 --decode; }
24
+ meta_value() {
25
+ local path="$1" key="$2"
26
+ [[ -f "$path" ]] || return 1
27
+ sed -n "s/^${key}=//p" "$path" | head -n1
28
+ }
29
+ valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
30
+ valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
31
+ valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
32
+ valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
33
+ valid_db_file() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ && "$1" != "." && "$1" != ".." ]]; }
34
+ valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
35
+
36
+ configure_registry_auth() {
37
+ [[ -n "$DEPLOY_USER" && "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
38
+ echo "Deployment account is invalid." >&2; exit 42;
39
+ }
40
+ local home config dir_mode file_mode deploy_uid
41
+ home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
42
+ [[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 42; }
43
+ config="$home/.docker"
44
+ if [[ ! -e "$config/config.json" ]]; then return 0; fi
45
+ [[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
46
+ echo "Docker credential path is not a regular protected directory/file." >&2; exit 42;
47
+ }
48
+ dir_mode="$(stat -c '%a' "$config")"
49
+ file_mode="$(stat -c '%a' "$config/config.json")"
50
+ deploy_uid="$(id -u "$DEPLOY_USER")"
51
+ if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
52
+ [[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
53
+ echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
54
+ exit 42
55
+ fi
56
+ export DOCKER_CONFIG="$config"
57
+ }
58
+
59
+ managed_runtime_info() {
60
+ local root state current file
61
+ root="$1"
62
+ state="$root/deployment.env"
63
+ MANAGED_CONTAINER=""
64
+ MANAGED_IMAGE=""
65
+ MANAGED_RUNNING=false
66
+ [[ -f "$state" && ! -L "$state" ]] || return 0
67
+ current="$(meta_value "$state" CURRENT_RELEASE || true)"
68
+ [[ -n "$current" ]] || { echo "Managed application state is malformed." >&2; exit 43; }
69
+ file="$root/releases/$current/release.env"
70
+ [[ -f "$file" && ! -L "$file" ]] || { echo "Managed application release metadata is missing." >&2; exit 43; }
71
+ MANAGED_CONTAINER="$(meta_value "$file" CONTAINER_NAME || true)"
72
+ MANAGED_IMAGE="$(meta_value "$file" IMAGE || true)"
73
+ [[ -n "$MANAGED_CONTAINER" && -n "$MANAGED_IMAGE" ]] || { echo "Managed application release metadata is malformed." >&2; exit 43; }
74
+ [[ "$(docker inspect --format '{{.State.Status}}' "$MANAGED_CONTAINER" 2>/dev/null || true)" == "running" ]] && MANAGED_RUNNING=true || true
75
+ }
76
+
77
+ inventory() {
78
+ local marker root owner env app run_as db_b64 db file present bytes backups running image count=0 sha
79
+ if [[ -d "$APP_ROOT" ]]; then
80
+ while IFS= read -r marker; do
81
+ [[ -n "$marker" ]] || continue
82
+ [[ ! -L "$marker" ]] || { echo "SQLite reservation marker must not be a symbolic link: $marker" >&2; exit 44; }
83
+ [[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" ]] || continue
84
+ owner="$(meta_value "$marker" OWNER_REPO || true)"
85
+ env="$(meta_value "$marker" ENVIRONMENT || true)"
86
+ app="$(meta_value "$marker" APP_SLUG || true)"
87
+ run_as="$(meta_value "$marker" RUN_AS || true)"
88
+ db_b64="$(meta_value "$marker" DATABASE_FILE_B64 || true)"
89
+ db="$(b64d "$db_b64")"
90
+ sha="$(meta_value "$marker" RESERVATION_SHA256 || true)"
91
+ [[ -n "$owner" && -n "$env" && -n "$app" && -n "$run_as" && -n "$db" && "$sha" =~ ^[0-9a-f]{64}$ ]] || {
92
+ echo "SQLite reservation marker is malformed: $marker" >&2; exit 44;
93
+ }
94
+ root="$(dirname "$marker")"
95
+ file="$root/data/$db"
96
+ present=false; bytes=0
97
+ if [[ -e "$file" ]]; then
98
+ [[ -f "$file" && ! -L "$file" ]] || { echo "Managed SQLite database is not a regular file: $file" >&2; exit 44; }
99
+ present=true
100
+ bytes="$(stat -c '%s' "$file")"
101
+ fi
102
+ backups=0
103
+ if [[ -d "$root/backups" && ! -L "$root/backups" ]]; then
104
+ backups="$(find "$root/backups" -maxdepth 1 -type f -printf '.' | wc -c)"
105
+ fi
106
+ managed_runtime_info "$root"
107
+ running="$MANAGED_RUNNING"; image="$MANAGED_IMAGE"
108
+ printf 'SQLITE\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
109
+ "$owner" "$env" "$app" "$run_as" "$db" "$sha" "$present" "$bytes" "$backups" "$running" "$image"
110
+ count=$((count + 1))
111
+ done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name persistence.env -print 2>/dev/null | sort)
112
+ fi
113
+ printf 'SUMMARY\tsqliteCount\t%s\n' "$count"
114
+ }
115
+
116
+ decode_spec() {
117
+ OWNER="$(b64d "$OWNER_B64")"
118
+ ENVIRONMENT="$(b64d "$ENV_B64")"
119
+ APP_SLUG="$(b64d "$APP_B64")"
120
+ IMAGE="$(b64d "$IMAGE_B64")"
121
+ RUN_AS="$(b64d "$RUN_AS_B64")"
122
+ DATABASE_FILE="$(b64d "$DB_FILE_B64")"
123
+ SOURCE_DATABASE="$(b64d "$SOURCE_DB_B64")"
124
+ valid_repo "$OWNER" || { echo "Invalid SQLite owner repository." >&2; exit 45; }
125
+ valid_slug "$ENVIRONMENT" || { echo "Invalid SQLite environment." >&2; exit 45; }
126
+ valid_slug "$APP_SLUG" || { echo "Invalid SQLite app slug." >&2; exit 45; }
127
+ valid_image "$IMAGE" || { echo "SQLite maintenance image is not an immutable digest reference." >&2; exit 45; }
128
+ valid_run_as "$RUN_AS" || { echo "Invalid SQLite runAs." >&2; exit 45; }
129
+ valid_db_file "$DATABASE_FILE" || { echo "Invalid SQLite database file name." >&2; exit 45; }
130
+ [[ "$RESERVATION_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid SQLite reservation fingerprint." >&2; exit 45; }
131
+ [[ "${#ENV_KEYS[@]}" -eq "${#ENV_VALUES[@]}" && "${#ENV_KEYS[@]}" -gt 0 ]] || { echo "Invalid SQLite maintenance environment payload." >&2; exit 45; }
132
+ local index key
133
+ for index in "${!ENV_KEYS[@]}"; do
134
+ key="$(b64d "${ENV_KEYS[$index]}")"
135
+ valid_env_key "$key" || { echo "Invalid SQLite maintenance environment key." >&2; exit 45; }
136
+ done
137
+ }
138
+
139
+ assert_marker() {
140
+ local marker="$1" expected="$2"
141
+ [[ -f "$marker" && ! -L "$marker" ]] || { echo "SQLite reservation is missing." >&2; exit 46; }
142
+ [[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" &&
143
+ "$(meta_value "$marker" OWNER_REPO || true)" == "$OWNER" &&
144
+ "$(meta_value "$marker" ENVIRONMENT || true)" == "$ENVIRONMENT" &&
145
+ "$(meta_value "$marker" APP_SLUG || true)" == "$APP_SLUG" &&
146
+ "$(meta_value "$marker" RUN_AS || true)" == "$RUN_AS" &&
147
+ "$(b64d "$(meta_value "$marker" DATABASE_FILE_B64 || true)")" == "$DATABASE_FILE" &&
148
+ "$(meta_value "$marker" RESERVATION_SHA256 || true)" == "$RESERVATION_SHA" ]] || {
149
+ echo "SQLite reservation ownership/configuration mismatch." >&2; exit 46;
150
+ }
151
+ [[ "$expected" != "NONE" && "$expected" == "$RESERVATION_SHA" ]] || {
152
+ echo "SQLite reservation changed after planning." >&2; exit 47;
153
+ }
154
+ }
155
+
156
+ write_marker() {
157
+ local marker tmp
158
+ marker="$1"
159
+ tmp="$marker.tmp.$$"
160
+ {
161
+ printf 'SCHEMA_VERSION=%s\n' "$SQLITE_SCHEMA_VERSION"
162
+ printf 'ADAPTER=sqlite\n'
163
+ printf 'OWNER_REPO=%s\n' "$OWNER"
164
+ printf 'ENVIRONMENT=%s\n' "$ENVIRONMENT"
165
+ printf 'APP_SLUG=%s\n' "$APP_SLUG"
166
+ printf 'RUN_AS=%s\n' "$RUN_AS"
167
+ printf 'DATABASE_FILE_B64=%s\n' "$DB_FILE_B64"
168
+ printf 'RESERVATION_SHA256=%s\n' "$RESERVATION_SHA"
169
+ } > "$tmp"
170
+ chmod 0600 "$tmp"
171
+ mv -f "$tmp" "$marker"
172
+ }
173
+
174
+ maintenance_args() {
175
+ local -n out_ref=$1
176
+ local index key value
177
+ out_ref=(docker run --rm --network none --user "$RUN_AS")
178
+ for index in "${!ENV_KEYS[@]}"; do
179
+ key="$(b64d "${ENV_KEYS[$index]}")"
180
+ value="$(b64d "${ENV_VALUES[$index]}")"
181
+ out_ref+=(--env "$key=$value")
182
+ done
183
+ }
184
+
185
+ run_backup() {
186
+ local source_dir="$1" backup_dir="$2" backup_name="$3"
187
+ local -a args
188
+ maintenance_args args
189
+ args+=(--mount "type=bind,src=$source_dir,dst=/data" --mount "type=bind,src=$backup_dir,dst=/backup" "$IMAGE" --backup --backup-path "/backup/$backup_name")
190
+ "${args[@]}"
191
+ }
192
+
193
+ run_check() {
194
+ local data_dir="$1"
195
+ local -a args
196
+ maintenance_args args
197
+ args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --check-rollback-compatibility)
198
+ "${args[@]}"
199
+ }
200
+
201
+ run_migrate() {
202
+ local data_dir="$1"
203
+ local -a args
204
+ maintenance_args args
205
+ args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --migrate)
206
+ [[ "$FIRST_INSTALL" == true ]] && args+=(--first-install)
207
+ "${args[@]}"
208
+ }
209
+
210
+ record_evidence() {
211
+ local root="$1" action="$2" outcome="$3" artifact="${4:-}" timestamp
212
+ timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
213
+ printf '{"timestamp":"%s","action":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","image":"%s","databaseFile":"%s","reservationSha256":"%s","artifact":"%s","outcome":"%s"}\n' \
214
+ "$timestamp" "$action" "$OWNER" "$ENVIRONMENT" "$APP_SLUG" "$IMAGE" "$DATABASE_FILE" "$RESERVATION_SHA" "$artifact" "$outcome" >> "$root/sqlite-evidence.jsonl"
215
+ chmod 0600 "$root/sqlite-evidence.jsonl"
216
+ }
217
+
218
+ set_paths() {
219
+ ROOT="$APP_ROOT/$APP_SLUG/$ENVIRONMENT"
220
+ DATA="$ROOT/data"
221
+ BACKUPS="$ROOT/backups"
222
+ MARKER="$ROOT/persistence.env"
223
+ LOCK_FILE="/run/lock/lean-poc-app-$APP_SLUG--$ENVIRONMENT.lock"
224
+ for path in "$ROOT" "$DATA" "$BACKUPS" "$MARKER" "$ROOT/deployment.env" "$ROOT/sqlite-evidence.jsonl"; do
225
+ [[ ! -L "$path" ]] || { echo "SQLite managed path must not be a symbolic link: $path" >&2; exit 48; }
226
+ done
227
+ }
228
+
229
+ expected_ids() {
230
+ EXPECTED_UID="${RUN_AS%%:*}"
231
+ [[ "$RUN_AS" == *:* ]] && EXPECTED_GID="${RUN_AS#*:}" || EXPECTED_GID="$EXPECTED_UID"
232
+ }
233
+
234
+ create_reserved_dirs() {
235
+ expected_ids
236
+ install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$APP_SLUG" "$ROOT"
237
+ if [[ ! -e "$DATA" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$DATA"; fi
238
+ if [[ ! -e "$BACKUPS" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$BACKUPS"; fi
239
+ verify_reserved_dirs
240
+ }
241
+
242
+ verify_reserved_dirs() {
243
+ expected_ids
244
+ [[ -d "$DATA" && ! -L "$DATA" && -d "$BACKUPS" && ! -L "$BACKUPS" ]] || {
245
+ echo "SQLite reserved data/backup directories are missing or invalid." >&2; exit 48;
246
+ }
247
+ [[ "$(stat -c '%u' "$DATA")" == "$EXPECTED_UID" && "$(stat -c '%g' "$DATA")" == "$EXPECTED_GID" &&
248
+ "$(stat -c '%u' "$BACKUPS")" == "$EXPECTED_UID" && "$(stat -c '%g' "$BACKUPS")" == "$EXPECTED_GID" ]] || {
249
+ echo "SQLite data/backup directory ownership does not match runAs." >&2; exit 48;
250
+ }
251
+ }
252
+
253
+ operate() {
254
+ decode_spec
255
+ configure_registry_auth
256
+ set_paths
257
+ exec 9>>"$LOCK_FILE"
258
+ flock -n 9 || { echo "Another app/persistence operation holds the lock for $APP_SLUG--$ENVIRONMENT." >&2; exit 75; }
259
+
260
+ local db="$DATA/$DATABASE_FILE" stamp backup_name source_real source_parent
261
+ managed_runtime_info "$ROOT"
262
+
263
+ case "$ACTION" in
264
+ import)
265
+ [[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "SQLite import destination was reserved after planning." >&2; exit 47; }
266
+ [[ ! -e "$ROOT/deployment.env" ]] || { echo "SQLite import must occur before the first managed application deployment." >&2; exit 49; }
267
+ [[ ! -e "$db" ]] || { echo "SQLite import refuses to overwrite an existing destination database." >&2; exit 49; }
268
+ [[ "$SOURCE_DATABASE" == /srv/* && "$SOURCE_DATABASE" != "$APP_ROOT"/* ]] || { echo "SQLite import source must be outside the Kit app root under /srv." >&2; exit 49; }
269
+ source_real="$(realpath -e "$SOURCE_DATABASE" 2>/dev/null || true)"
270
+ [[ "$source_real" == "$SOURCE_DATABASE" && -f "$SOURCE_DATABASE" && ! -L "$SOURCE_DATABASE" ]] || { echo "SQLite import source must be an existing non-symlink regular file with no symlinked path components." >&2; exit 49; }
271
+ [[ "$(basename "$SOURCE_DATABASE")" == "$DATABASE_FILE" ]] || { echo "SQLite import source file name mismatch." >&2; exit 49; }
272
+ create_reserved_dirs
273
+ docker pull "$IMAGE" >/dev/null
274
+ source_parent="$(dirname "$SOURCE_DATABASE")"
275
+ run_backup "$source_parent" "$DATA" "$DATABASE_FILE"
276
+ run_check "$DATA"
277
+ write_marker "$MARKER"
278
+ record_evidence "$ROOT" import imported "$SOURCE_DATABASE"
279
+ printf 'SQLite import completed and verified: %s\n' "$db"
280
+ ;;
281
+ backup)
282
+ assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
283
+ verify_reserved_dirs
284
+ [[ -f "$db" && ! -L "$db" ]] || { echo "SQLite backup requires the reserved database file." >&2; exit 49; }
285
+ docker pull "$IMAGE" >/dev/null
286
+ stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
287
+ backup_name="backup-$stamp-$DATABASE_FILE"
288
+ run_backup "$DATA" "$BACKUPS" "$backup_name"
289
+ record_evidence "$ROOT" backup backed-up "$backup_name"
290
+ printf 'SQLite verified backup created: %s\n' "$BACKUPS/$backup_name"
291
+ ;;
292
+ migrate)
293
+ if [[ "$FIRST_INSTALL" == true ]]; then
294
+ [[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "First-install SQLite destination was reserved after planning." >&2; exit 47; }
295
+ [[ ! -e "$ROOT/deployment.env" && ! -e "$db" ]] || { echo "First-install SQLite migration requires no runtime state or database." >&2; exit 49; }
296
+ create_reserved_dirs
297
+ docker pull "$IMAGE" >/dev/null
298
+ run_migrate "$DATA"
299
+ run_check "$DATA"
300
+ write_marker "$MARKER"
301
+ record_evidence "$ROOT" migrate migrated-first-install ""
302
+ printf 'SQLite first-install migration completed and verified.\n'
303
+ else
304
+ assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
305
+ verify_reserved_dirs
306
+ [[ -f "$db" && ! -L "$db" ]] || { echo "SQLite migration requires the reserved database file." >&2; exit 49; }
307
+ docker pull "$IMAGE" >/dev/null
308
+ stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
309
+ backup_name="pre-migrate-$stamp-$DATABASE_FILE"
310
+ run_backup "$DATA" "$BACKUPS" "$backup_name"
311
+ if [[ "$MANAGED_RUNNING" == true ]]; then docker stop "$MANAGED_CONTAINER" >/dev/null; fi
312
+ if ! run_migrate "$DATA"; then
313
+ record_evidence "$ROOT" migrate failed "$backup_name"
314
+ echo "SQLite migration failed; verified pre-migration backup is retained and managed runtime remains stopped." >&2
315
+ exit 50
316
+ fi
317
+ run_check "$DATA"
318
+ record_evidence "$ROOT" migrate migrated "$backup_name"
319
+ printf 'SQLite migration completed; verified pre-migration backup: %s\n' "$BACKUPS/$backup_name"
320
+ fi
321
+ ;;
322
+ rollback-check)
323
+ assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
324
+ verify_reserved_dirs
325
+ [[ -f "$db" && ! -L "$db" ]] || { echo "SQLite rollback check requires the reserved database file." >&2; exit 49; }
326
+ docker pull "$IMAGE" >/dev/null
327
+ run_check "$DATA"
328
+ record_evidence "$ROOT" rollback-check compatible ""
329
+ printf 'SQLite rollback compatibility check passed for image %s.\n' "$IMAGE"
330
+ ;;
331
+ *) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
332
+ esac
333
+ }
334
+
335
+ case "$ACTION" in
336
+ inventory) inventory ;;
337
+ import|backup|migrate|rollback-check) operate ;;
338
+ *) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
339
+ esac
340
+ '''
@@ -0,0 +1,86 @@
1
+ """CLI for the SQLite persistence adapter."""
2
+ from __future__ import annotations
3
+ import argparse, json, sys
4
+ from typing import Any
5
+ from .errors import DeploymentKitError
6
+ from .sqlite_adapter import authorization_phrase, apply_sqlite_operation, normalize_sqlite_request, plan_sqlite_operation, sqlite_inventory
7
+ from .sqlite_import_recovery import (
8
+ apply_sqlite_import_recovery,
9
+ authorization_phrase as recovery_authorization_phrase,
10
+ plan_sqlite_import_recovery,
11
+ )
12
+
13
+
14
+ def _print(value: Any, *, as_json: bool) -> None:
15
+ if as_json:
16
+ print(json.dumps(value, indent=2, sort_keys=True)); return
17
+ if isinstance(value, dict):
18
+ for key, item in value.items():
19
+ print(f"{key}: {json.dumps(item, sort_keys=True) if isinstance(item,(dict,list)) else item}")
20
+ else: print(value)
21
+
22
+
23
+ def _parse_env(items: list[str]) -> list[tuple[str,str]]:
24
+ out=[]
25
+ for item in items:
26
+ if '=' not in item: raise DeploymentKitError("--maintenance-env entries must use NAME=value form.")
27
+ out.append(tuple(item.split('=',1)))
28
+ return out
29
+
30
+
31
+ def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
32
+ if args.yes: return
33
+ if input(f"Type {phrase} to {action}: ").strip() != phrase:
34
+ raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
35
+
36
+
37
+ def _common(parser: argparse.ArgumentParser) -> None:
38
+ parser.add_argument('name')
39
+ parser.add_argument('--owner-repo', required=True)
40
+ parser.add_argument('--environment', required=True)
41
+ parser.add_argument('--app-slug', required=True)
42
+ parser.add_argument('--image', required=True)
43
+ parser.add_argument('--maintenance-env', action='append', default=[], help='Non-secret image maintenance environment value in NAME=value form; repeat as needed')
44
+ parser.add_argument('--json', action='store_true')
45
+ parser.add_argument('--yes', action='store_true')
46
+
47
+
48
+ def _import_args(parser: argparse.ArgumentParser) -> None:
49
+ parser.add_argument('--run-as', required=True)
50
+ parser.add_argument('--database-file', required=True)
51
+ parser.add_argument('--source-database', required=True)
52
+
53
+
54
+ def build_parser() -> argparse.ArgumentParser:
55
+ parser=argparse.ArgumentParser(prog='lean-poc-deploy sqlite')
56
+ sub=parser.add_subparsers(dest='command',required=True)
57
+ inv=sub.add_parser('inventory',help='Read SQLite persistence reservations without writes'); inv.add_argument('name'); inv.add_argument('--json',action='store_true')
58
+ imp=sub.add_parser('import',help='Import an existing SQLite DB via the exact image online-backup contract'); _common(imp); _import_args(imp)
59
+ recover=sub.add_parser('recover-import',help='Recover a verified failed import without overwriting or recopying the database'); _common(recover); _import_args(recover)
60
+ backup=sub.add_parser('backup',help='Create a verified SQLite backup with the exact image'); _common(backup)
61
+ migrate=sub.add_parser('migrate',help='Create required backup and run exact-image SQLite migration'); _common(migrate)
62
+ migrate.add_argument('--first-install',action='store_true'); migrate.add_argument('--run-as'); migrate.add_argument('--database-file')
63
+ check=sub.add_parser('rollback-check',help='Verify that an exact rollback image accepts the current SQLite state'); _common(check)
64
+ return parser
65
+
66
+
67
+ def main(argv: list[str]|None=None) -> int:
68
+ args=build_parser().parse_args(argv)
69
+ try:
70
+ if args.command=='inventory': _print(sqlite_inventory(args.name),as_json=args.json); return 0
71
+ request=normalize_sqlite_request(owner_repo=args.owner_repo,environment=args.environment,app_slug=args.app_slug,image=args.image,
72
+ maintenance_environment=_parse_env(args.maintenance_env),run_as=getattr(args,'run_as',None),database_file=getattr(args,'database_file',None),
73
+ source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False))
74
+ if args.command=='recover-import':
75
+ plan=plan_sqlite_import_recovery(args.name,request)
76
+ _print({k:v for k,v in plan.items() if k!='request'},as_json=False)
77
+ phrase=recovery_authorization_phrase(plan)
78
+ _confirm(args,phrase,f"recover SQLite import for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
79
+ _print(apply_sqlite_import_recovery(plan),as_json=args.json); return 0
80
+ plan=plan_sqlite_operation(args.name,args.command,request)
81
+ _print({k:v for k,v in plan.items() if k!='spec'},as_json=False)
82
+ phrase=authorization_phrase(plan)
83
+ _confirm(args,phrase,f"run SQLite {args.command} for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
84
+ _print(apply_sqlite_operation(plan),as_json=args.json); return 0
85
+ except DeploymentKitError as error:
86
+ print(f"ERROR: {error}",file=sys.stderr); return 2