rocketdoo 2.2.4__tar.gz → 2.3.1__tar.gz

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 (56) hide show
  1. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/LICENSE +1 -1
  2. {rocketdoo-2.2.4/rocketdoo.egg-info → rocketdoo-2.3.1}/PKG-INFO +3 -3
  3. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/README.md +1 -1
  4. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/pyproject.toml +1 -1
  5. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/cli.py +8 -0
  6. rocketdoo-2.3.1/rocketdoo/pack_environment.py +459 -0
  7. rocketdoo-2.3.1/rocketdoo/unpack_environment.py +559 -0
  8. {rocketdoo-2.2.4 → rocketdoo-2.3.1/rocketdoo.egg-info}/PKG-INFO +3 -3
  9. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo.egg-info/SOURCES.txt +2 -0
  10. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/MANIFEST.in +0 -0
  11. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/__init__.py +0 -0
  12. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/config.py +0 -0
  13. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/__init__.py +0 -0
  14. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/config_loader.py +0 -0
  15. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/__init__.py +0 -0
  16. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/base.py +0 -0
  17. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/config_manager.py +0 -0
  18. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/module_packager.py +0 -0
  19. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/odoo_sh.py +0 -0
  20. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/deploy/vps.py +0 -0
  21. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/edition_setup.py +0 -0
  22. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/generator.py +0 -0
  23. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/gitman_config.py +0 -0
  24. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/module_scanner.py +0 -0
  25. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/port_validation.py +0 -0
  26. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/ssh_manager.py +0 -0
  27. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/core/utils.py +0 -0
  28. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/delete_identifiers.py +0 -0
  29. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/deploy_cli.py +0 -0
  30. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/docker_cli.py +0 -0
  31. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/init_project.py +0 -0
  32. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/main.py +0 -0
  33. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/project_info.py +0 -0
  34. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/rocketdoo.py +0 -0
  35. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/scaffold.py +0 -0
  36. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/.devcontainer/devcontainer.json +0 -0
  37. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/.devcontainer/docker-compose.yaml +0 -0
  38. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/.vscode/launch.json +0 -0
  39. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/.vscode/launch.json.jinja +0 -0
  40. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/.vscode/settings.json +0 -0
  41. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/Dockerfile.jinja +0 -0
  42. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/addons/.gitkeep +0 -0
  43. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/config/odoo.conf +0 -0
  44. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/config/odoo.conf.jinja +0 -0
  45. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/deploy/.deployignore +0 -0
  46. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/deploy/deploy.yaml +0 -0
  47. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/deploy/deploy.yaml.jinja +0 -0
  48. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/docker-compose.yaml.jinja +0 -0
  49. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/install_dependencies.sh +0 -0
  50. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/templates/odoo_pg_pass +0 -0
  51. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo/welcome.py +0 -0
  52. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo.egg-info/dependency_links.txt +0 -0
  53. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo.egg-info/entry_points.txt +0 -0
  54. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo.egg-info/requires.txt +0 -0
  55. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/rocketdoo.egg-info/top_level.txt +0 -0
  56. {rocketdoo-2.2.4 → rocketdoo-2.3.1}/setup.cfg +0 -0
@@ -57,7 +57,7 @@ If the Library as you received it specifies that a proxy can decide whether futu
57
57
  <div RKD as ROCKETDOO V2=""></div>
58
58
 
59
59
  Licencia: LGPL-3.0+
60
- Versión: "2.2.4"
60
+ Versión: "2.3.1"
61
61
  Autor: Horacio Montaño, Elias Braceras
62
62
  Fecha: 16/10/2024
63
63
  Descripción: Framework to development Odoo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocketdoo
3
- Version: 2.2.4
3
+ Version: 2.3.1
4
4
  Summary: Framework for creating Odoo development environments with Docker and custom templates.
5
5
  Author-email: Horacio Montaño <horaciomontano@hdmsoft.com.ar>
6
6
  License: GNU LESSER GENERAL PUBLIC LICENSE
@@ -62,7 +62,7 @@ License: GNU LESSER GENERAL PUBLIC LICENSE
62
62
  <div RKD as ROCKETDOO V2=""></div>
63
63
 
64
64
  Licencia: LGPL-3.0+
65
- Versión: "2.2.4"
65
+ Versión: "2.3.1"
66
66
  Autor: Horacio Montaño, Elias Braceras
67
67
  Fecha: 16/10/2024
68
68
  Descripción: Framework to development Odoo
@@ -95,7 +95,7 @@ Odoo Development Framework
95
95
  - "Horacio Montaño" and "Elias Braceras"
96
96
 
97
97
  ## Version:
98
- - "2.2.4"
98
+ - "2.3.1"
99
99
 
100
100
  ----------------------------------------------------------------------------------------------------------------------------------------------------------
101
101
 
@@ -15,7 +15,7 @@ Odoo Development Framework
15
15
  - "Horacio Montaño" and "Elias Braceras"
16
16
 
17
17
  ## Version:
18
- - "2.2.4"
18
+ - "2.3.1"
19
19
 
20
20
  ----------------------------------------------------------------------------------------------------------------------------------------------------------
21
21
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rocketdoo"
3
- version = "2.2.4"
3
+ version = "2.3.1"
4
4
  description = "Framework for creating Odoo development environments with Docker and custom templates."
5
5
  authors = [
6
6
  { name = "Horacio Montaño", email = "horaciomontano@hdmsoft.com.ar" }
@@ -19,6 +19,8 @@ from rocketdoo.deploy_cli import (
19
19
  deploy_run,
20
20
  validate_modules
21
21
  )
22
+ from rocketdoo.pack_environment import pack_environment
23
+ from rocketdoo.unpack_environment import unpack_environment
22
24
 
23
25
  # Detect the command name used to invoke the CLI
24
26
  PROG_NAME = "rkd" if "rkd" in sys.argv[0] else "rocketdoo"
@@ -425,6 +427,12 @@ main.add_command(logs)
425
427
  main.add_command(build)
426
428
  main.add_command(delete_command)
427
429
 
430
+ # ============================================================
431
+ # 📦 Register Share/Unpack commands
432
+ # ============================================================
433
+ main.add_command(pack_environment, name="pack")
434
+ main.add_command(unpack_environment, name="unpack")
435
+
428
436
  # ============================================================
429
437
  # 🚀 Register Deploy commands as Rocketdoo subcommands
430
438
  # ============================================================
@@ -0,0 +1,459 @@
1
+ # rocketdoo/pack_environment.py
2
+ """
3
+ rkd pack — Packages the development environment to share with another developer.
4
+
5
+ Steps:
6
+ 1. Validates that a Rocketdoo project exists in the current directory.
7
+ 2. Backs up the active database + filestore via pg_dump inside the container.
8
+ 3. Sanitizes the Dockerfile: comments out SSH key lines to avoid exposing them.
9
+ 4. Excludes the .ssh/ directory from the ZIP (must never be shared).
10
+ 5. Creates rkd-shared.json with environment metadata (flags private repo usage).
11
+ 6. Compresses everything into a shareable ZIP file.
12
+ 7. Restores the original Dockerfile after compression.
13
+ """
14
+
15
+ import re
16
+ import json
17
+ import subprocess
18
+ import zipfile
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ import click
23
+ import questionary
24
+ from rich.console import Console
25
+ from rich.panel import Panel
26
+ from rich import box
27
+
28
+ from rocketdoo.project_info import get_project_info, project_exists, read_docker_compose
29
+
30
+ console = Console()
31
+
32
+ # ─────────────────────────────────────────────────────────────
33
+ # Internal helpers
34
+ # ─────────────────────────────────────────────────────────────
35
+
36
+ def _get_db_container(compose_data: dict) -> str | None:
37
+ """Returns the database container name from docker-compose data."""
38
+ try:
39
+ return compose_data["services"]["db"]["container_name"]
40
+ except (KeyError, TypeError):
41
+ return None
42
+
43
+
44
+ def _get_odoo_container(compose_data: dict) -> str | None:
45
+ """Returns the Odoo web container name from docker-compose data."""
46
+ try:
47
+ return compose_data["services"]["web"]["container_name"]
48
+ except (KeyError, TypeError):
49
+ return None
50
+
51
+
52
+ def _is_container_running(container_name: str) -> bool:
53
+ """Checks whether a Docker container is currently running."""
54
+ try:
55
+ result = subprocess.run(
56
+ ["docker", "inspect", "--format", "{{.State.Running}}", container_name],
57
+ capture_output=True, text=True
58
+ )
59
+ return result.stdout.strip() == "true"
60
+ except Exception:
61
+ return False
62
+
63
+
64
+ def _list_odoo_databases(db_container: str) -> list[str]:
65
+ """Lists available databases in the PostgreSQL container."""
66
+ try:
67
+ result = subprocess.run(
68
+ [
69
+ "docker", "exec", db_container,
70
+ "psql", "-U", "root", "-d", "postgres",
71
+ "-t", "-c",
72
+ "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres';"
73
+ ],
74
+ capture_output=True, text=True
75
+ )
76
+ return [line.strip() for line in result.stdout.splitlines() if line.strip()]
77
+ except Exception:
78
+ return []
79
+
80
+
81
+ def _backup_database(db_container: str, db_name: str, output_path: Path) -> bool:
82
+ """
83
+ Runs pg_dump inside the container and saves the result to output_path.
84
+ Returns True if the backup succeeded.
85
+ """
86
+ console.print(f" [dim]Running pg_dump for '[cyan]{db_name}[/cyan]'...[/dim]")
87
+ try:
88
+ with open(output_path, "wb") as f:
89
+ result = subprocess.run(
90
+ [
91
+ "docker", "exec", db_container,
92
+ "pg_dump", "-U", "root", "--format=custom", db_name
93
+ ],
94
+ stdout=f,
95
+ stderr=subprocess.PIPE
96
+ )
97
+ if result.returncode != 0:
98
+ console.print(f" [red]✗ pg_dump error:[/red] {result.stderr.decode()}")
99
+ return False
100
+ size_mb = output_path.stat().st_size / (1024 * 1024)
101
+ console.print(f" [green]✓[/green] Backup saved: [yellow]{output_path.name}[/yellow] ({size_mb:.1f} MB)")
102
+ return True
103
+ except Exception as e:
104
+ console.print(f" [red]✗ Exception during database backup:[/red] {e}")
105
+ return False
106
+
107
+
108
+ def _backup_filestore(odoo_container: str, db_name: str, output_path: Path) -> bool:
109
+ """
110
+ Copies and compresses the Odoo filestore from the container to the host.
111
+
112
+ Supports multiple filestore layouts:
113
+ - /var/lib/odoo/.local/share/Odoo/filestore/<db_name>
114
+ - /var/lib/odoo/filestore/<db_name>
115
+ """
116
+ console.print(f" [dim]Copying filestore from container...[/dim]")
117
+
118
+ POSSIBLE_PATHS = [
119
+ f"/var/lib/odoo/.local/share/Odoo/filestore/{db_name}",
120
+ f"/var/lib/odoo/filestore/{db_name}",
121
+ ]
122
+
123
+ filestore_path = None
124
+ filestore_base = None
125
+
126
+ try:
127
+ # ── Detect filestore path dynamically ──
128
+ for path in POSSIBLE_PATHS:
129
+ check = subprocess.run(
130
+ ["docker", "exec", odoo_container, "test", "-d", path],
131
+ capture_output=True
132
+ )
133
+ if check.returncode == 0:
134
+ filestore_path = path
135
+ filestore_base = str(Path(path).parent)
136
+ break
137
+
138
+ if not filestore_path:
139
+ console.print(
140
+ f" [yellow]⚠[/yellow] Filestore not found for database "
141
+ f"[cyan]{db_name}[/cyan] in any known location — skipping."
142
+ )
143
+ return True # Not fatal
144
+
145
+ console.print(f" [dim]Detected filestore path:[/dim] [cyan]{filestore_path}[/cyan]")
146
+
147
+ # ── Compress filestore ──
148
+ with open(output_path, "wb") as f:
149
+ result = subprocess.run(
150
+ [
151
+ "docker", "exec", odoo_container,
152
+ "tar", "-czf", "-", "-C",
153
+ filestore_base,
154
+ db_name
155
+ ],
156
+ stdout=f,
157
+ stderr=subprocess.PIPE
158
+ )
159
+
160
+ if result.returncode != 0:
161
+ console.print(f" [red]✗ Error copying filestore:[/red] {result.stderr.decode()}")
162
+ return False
163
+
164
+ size_mb = output_path.stat().st_size / (1024 * 1024)
165
+ console.print(
166
+ f" [green]✓[/green] Filestore compressed: "
167
+ f"[yellow]{output_path.name}[/yellow] ({size_mb:.1f} MB)"
168
+ )
169
+
170
+ return True
171
+
172
+ except Exception as e:
173
+ console.print(f" [red]✗ Exception during filestore backup:[/red] {e}")
174
+ return False
175
+
176
+
177
+ def _sanitize_dockerfile(dockerfile_path: Path) -> str:
178
+ """
179
+ Reads the Dockerfile, comments out all SSH key related lines,
180
+ and returns the ORIGINAL content so it can be restored afterwards.
181
+ Writes the sanitized version to disk.
182
+ """
183
+ original_content = dockerfile_path.read_text()
184
+
185
+ ssh_patterns = [
186
+ r"^RUN mkdir -p /root/\.ssh",
187
+ r"^COPY \./.ssh/",
188
+ r"^RUN chmod \d+ /root/\.ssh/",
189
+ r"^RUN echo .StrictHostKeyChecking",
190
+ ]
191
+
192
+ sanitized_lines = []
193
+ for line in original_content.splitlines():
194
+ stripped = line.strip()
195
+ is_ssh_line = any(re.match(pat, stripped) for pat in ssh_patterns)
196
+ if is_ssh_line:
197
+ sanitized_lines.append(f"# [RKD-SANITIZED] {line}")
198
+ else:
199
+ sanitized_lines.append(line)
200
+
201
+ dockerfile_path.write_text("\n".join(sanitized_lines))
202
+ return original_content
203
+
204
+
205
+ def _restore_dockerfile(dockerfile_path: Path, original_content: str):
206
+ """Restores the Dockerfile to its original content."""
207
+ dockerfile_path.write_text(original_content)
208
+
209
+
210
+ def _verify_no_ssh_in_zip(zip_path: Path) -> list[str]:
211
+ """
212
+ Safety double-check: scans the ZIP for files that look like private SSH keys.
213
+ Returns a list of suspicious file names found.
214
+ """
215
+ suspicious = []
216
+ with zipfile.ZipFile(zip_path, "r") as zf:
217
+ for name in zf.namelist():
218
+ basename = Path(name).name
219
+ if any([
220
+ basename.startswith("id_rsa") and not basename.endswith(".pub"),
221
+ basename.startswith("id_ed25519") and not basename.endswith(".pub"),
222
+ basename.startswith("id_ecdsa") and not basename.endswith(".pub"),
223
+ "/.ssh/" in name and not name.endswith(".pub"),
224
+ ]):
225
+ suspicious.append(name)
226
+ return suspicious
227
+
228
+
229
+ def _create_zip(project_dir: Path, zip_path: Path, backup_dir: Path, exclude_dirs: list[str]) -> int:
230
+ """
231
+ Creates the ZIP archive of the full environment.
232
+ Excludes: .ssh/, __pycache__, node_modules, and any extra dirs provided.
233
+ Includes the backup directory contents under rkd_backups/.
234
+ Returns the number of files included.
235
+ """
236
+ ALWAYS_EXCLUDE = {".ssh", "__pycache__", "node_modules", ".mypy_cache"}
237
+ excluded = ALWAYS_EXCLUDE | set(exclude_dirs)
238
+
239
+ file_count = 0
240
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
241
+ for item in project_dir.rglob("*"):
242
+ rel = item.relative_to(project_dir)
243
+ if set(rel.parts) & excluded:
244
+ continue
245
+ if item.is_file():
246
+ zf.write(item, rel)
247
+ file_count += 1
248
+
249
+ if backup_dir.exists() and not backup_dir.is_relative_to(project_dir):
250
+ for bf in backup_dir.iterdir():
251
+ if bf.is_file():
252
+ zf.write(bf, Path("rkd_backups") / bf.name)
253
+ file_count += 1
254
+
255
+ return file_count
256
+
257
+
258
+ # ─────────────────────────────────────────────────────────────
259
+ # Main command
260
+ # ─────────────────────────────────────────────────────────────
261
+
262
+ @click.command(name="pack")
263
+ @click.option("--no-db", is_flag=True, default=False,
264
+ help="Skip the database and filestore backup (environment files only).")
265
+ @click.option("--output", "-o", default=None, type=click.Path(),
266
+ help="Output path for the ZIP file (default: parent directory, named after the project).")
267
+ @click.option("--db-name", default=None,
268
+ help="Name of the database to back up (useful when multiple databases exist).")
269
+ def pack_environment(no_db, output, db_name):
270
+ """
271
+ 📦 Package the development environment to share with another developer.
272
+
273
+ Generates a ZIP with the full environment directory, a database backup,
274
+ and the filestore — sanitizing SSH keys so they are never exposed.
275
+
276
+ \b
277
+ Examples:
278
+ rkd pack → full backup + ZIP
279
+ rkd pack --no-db → environment only (no DB backup)
280
+ rkd pack -o /tmp/my.zip → specify output path
281
+ """
282
+ console.print()
283
+ console.print(Panel(
284
+ "[bold cyan]📦 RKD Pack — Prepare environment for sharing[/bold cyan]\n\n"
285
+ "[dim]This process will:[/dim]\n"
286
+ " [green]✓[/green] Back up the database and filestore\n"
287
+ " [green]✓[/green] Sanitize SSH keys from the Dockerfile\n"
288
+ " [green]✓[/green] Generate a shareable ZIP file",
289
+ border_style="cyan",
290
+ box=box.ROUNDED
291
+ ))
292
+ console.print()
293
+
294
+ # ── 1. Validate project ──
295
+ if not project_exists():
296
+ console.print("[red]✗[/red] No Rocketdoo project detected in this directory.")
297
+ console.print("[dim]💡 Run [cyan]rkd init[/cyan] first.[/dim]")
298
+ return
299
+
300
+ project_dir = Path.cwd()
301
+ project_info = get_project_info()
302
+ project_name = project_info.get("project_name") or project_dir.name
303
+ compose_data = read_docker_compose()
304
+
305
+ console.print(f"[bold]Project detected:[/bold] [cyan]{project_name}[/cyan]")
306
+ console.print(f"[bold]Odoo:[/bold] {project_info.get('odoo_version', 'unknown')} "
307
+ f"({project_info.get('odoo_edition', 'Community')})")
308
+ console.print()
309
+
310
+ # ── 2. Database and filestore backup ──
311
+ backup_dir = project_dir / "rkd_backups"
312
+ db_backup_path = None
313
+ fs_backup_path = None
314
+
315
+ if not no_db:
316
+ db_container = _get_db_container(compose_data) if compose_data else None
317
+ odoo_container = _get_odoo_container(compose_data) if compose_data else None
318
+
319
+ if not db_container:
320
+ console.print("[yellow]⚠[/yellow] Could not detect the database container.")
321
+ console.print("[dim] Continuing without DB backup. Use [cyan]--no-db[/cyan] to suppress this warning.[/dim]")
322
+ elif not _is_container_running(db_container):
323
+ console.print(f"[yellow]⚠[/yellow] Container [cyan]{db_container}[/cyan] is not running.")
324
+ console.print("[dim] Start the environment with [cyan]rkd up -d[/cyan] before running pack with backup.[/dim]")
325
+ if not questionary.confirm("Continue anyway without DB backup?", default=False).ask():
326
+ console.print("[yellow]Operation cancelled.[/yellow]")
327
+ return
328
+ else:
329
+ console.print("[bold]💾 Database backup:[/bold]")
330
+ available_dbs = _list_odoo_databases(db_container)
331
+
332
+ if not available_dbs:
333
+ console.print(" [yellow]⚠[/yellow] No Odoo databases found.")
334
+ else:
335
+ if db_name and db_name in available_dbs:
336
+ selected_db = db_name
337
+ elif len(available_dbs) == 1:
338
+ selected_db = available_dbs[0]
339
+ console.print(f" Database detected: [cyan]{selected_db}[/cyan]")
340
+ else:
341
+ selected_db = questionary.select(
342
+ "Select the database to back up:",
343
+ choices=available_dbs
344
+ ).ask()
345
+
346
+ backup_dir.mkdir(exist_ok=True)
347
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
348
+ db_backup_path = backup_dir / f"db_{selected_db}_{timestamp}.dump"
349
+ fs_backup_path = backup_dir / f"filestore_{selected_db}_{timestamp}.tar.gz"
350
+
351
+ db_ok = _backup_database(db_container, selected_db, db_backup_path)
352
+ if not db_ok:
353
+ db_backup_path = None
354
+
355
+ if odoo_container and _is_container_running(odoo_container):
356
+ _backup_filestore(odoo_container, selected_db, fs_backup_path)
357
+ else:
358
+ console.print(f" [yellow]⚠[/yellow] Odoo container [cyan]{odoo_container}[/cyan] is not running — filestore skipped.")
359
+ fs_backup_path = None
360
+
361
+ # ── 3. Detect SSH key usage ──
362
+ uses_ssh = project_info.get("use_private_repos", False)
363
+ ssh_key_name = project_info.get("ssh_key")
364
+
365
+ if uses_ssh:
366
+ console.print()
367
+ console.print("[bold]🔐 SSH keys detected:[/bold]")
368
+ console.print(f" Key in use: [yellow]{ssh_key_name or 'detected in Dockerfile'}[/yellow]")
369
+ console.print(" [dim]SSH keys will be excluded from the ZIP.[/dim]")
370
+
371
+ # ── 4. Sanitize Dockerfile ──
372
+ dockerfile_path = project_dir / "Dockerfile"
373
+ original_dockerfile = None
374
+
375
+ if dockerfile_path.exists() and uses_ssh:
376
+ console.print()
377
+ console.print("[bold]🧹 Sanitizing Dockerfile...[/bold]")
378
+ original_dockerfile = _sanitize_dockerfile(dockerfile_path)
379
+ console.print(" [green]✓[/green] SSH lines commented out in the Dockerfile for the ZIP.")
380
+
381
+ # ── 5. Write rkd-shared.json metadata ──
382
+ shared_meta = {
383
+ "rkd_shared": True,
384
+ "packed_at": datetime.now().isoformat(),
385
+ "project_name": project_name,
386
+ "odoo_version": project_info.get("odoo_version"),
387
+ "odoo_edition": project_info.get("odoo_edition", "Community"),
388
+ "db_version": project_info.get("db_version"),
389
+ "odoo_port": project_info.get("odoo_port"),
390
+ "vsc_port": project_info.get("vsc_port"),
391
+ "uses_private_repos": uses_ssh,
392
+ "ssh_key_name": ssh_key_name,
393
+ "has_db_backup": db_backup_path is not None and db_backup_path.exists(),
394
+ "has_filestore_backup": fs_backup_path is not None and fs_backup_path.exists(),
395
+ }
396
+
397
+ meta_path = project_dir / "rkd-shared.json"
398
+ meta_path.write_text(json.dumps(shared_meta, indent=2, ensure_ascii=False))
399
+ console.print()
400
+ console.print("[dim]📋 Environment metadata written to rkd-shared.json[/dim]")
401
+
402
+ # ── 6. Create ZIP ──
403
+ console.print()
404
+ console.print("[bold]🗜️ Creating ZIP...[/bold]")
405
+
406
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
407
+ if output:
408
+ zip_path = Path(output)
409
+ else:
410
+ zip_path = project_dir.parent / f"{project_name}_rkd_shared_{timestamp}.zip"
411
+
412
+ try:
413
+ file_count = _create_zip(
414
+ project_dir=project_dir,
415
+ zip_path=zip_path,
416
+ backup_dir=backup_dir,
417
+ exclude_dirs=[".ssh"]
418
+ )
419
+ except Exception as e:
420
+ console.print(f"[red]✗ Error creating ZIP:[/red] {e}")
421
+ if original_dockerfile:
422
+ _restore_dockerfile(dockerfile_path, original_dockerfile)
423
+ meta_path.unlink(missing_ok=True)
424
+ return
425
+
426
+ # ── 7. Restore original Dockerfile ──
427
+ if original_dockerfile:
428
+ _restore_dockerfile(dockerfile_path, original_dockerfile)
429
+ console.print("[dim] Original Dockerfile restored.[/dim]")
430
+
431
+ # Clean up temporary metadata from working directory
432
+ meta_path.unlink(missing_ok=True)
433
+
434
+ # ── 8. SSH safety verification ──
435
+ suspicious = _verify_no_ssh_in_zip(zip_path)
436
+ if suspicious:
437
+ console.print()
438
+ console.print("[bold red]⚠️ SECURITY WARNING:[/bold red]")
439
+ console.print("[red]Possible private SSH keys detected inside the ZIP:[/red]")
440
+ for s in suspicious:
441
+ console.print(f" [red]• {s}[/red]")
442
+ console.print("[dim]Please review the ZIP contents before sharing.[/dim]")
443
+
444
+ # ── 9. Final summary ──
445
+ zip_size_mb = zip_path.stat().st_size / (1024 * 1024)
446
+ console.print()
447
+ console.print(Panel(
448
+ f"[bold green]✅ Environment packaged successfully[/bold green]\n\n"
449
+ f"[bold]📁 File:[/bold] [cyan]{zip_path}[/cyan]\n"
450
+ f"[bold]📦 Files included:[/bold] {file_count}\n"
451
+ f"[bold]💾 Size:[/bold] {zip_size_mb:.1f} MB\n"
452
+ f"[bold]🔐 SSH Keys:[/bold] {'excluded ✓' if uses_ssh else 'not applicable'}\n"
453
+ f"[bold]💿 DB Backup:[/bold] {'included ✓' if db_backup_path else ('skipped (--no-db)' if no_db else 'not available')}\n\n"
454
+ f"[dim]Share the ZIP with the other developer.\n"
455
+ f"The recipient should run: [cyan]rkd unpack[/cyan][/dim]",
456
+ border_style="green",
457
+ box=box.ROUNDED
458
+ ))
459
+ console.print()
@@ -0,0 +1,559 @@
1
+ # rocketdoo/unpack_environment.py
2
+ """
3
+ rkd unpack — Starts a development environment shared by another developer.
4
+
5
+ Steps:
6
+ 1. Detects rkd-shared.json to confirm this is a shared environment.
7
+ 2. Validates that the environment's ports are available; suggests alternatives if not.
8
+ 3. If the environment used private repos (SSH), lists the recipient's keys and
9
+ configures the Dockerfile with the chosen one.
10
+ 4. Starts the environment with docker compose up -d.
11
+ 5. If a database backup is present, automatically restores the DB and filestore.
12
+ """
13
+
14
+ import json
15
+ import re
16
+ import subprocess
17
+ import time
18
+ from pathlib import Path
19
+
20
+ import click
21
+ import questionary
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+ from rich import box
26
+
27
+ from rocketdoo.core.port_validation import is_port_in_use, find_available_port
28
+ from rocketdoo.core.ssh_manager import list_private_keys, copy_key_to_build_context, inject_ssh_into_dockerfile
29
+ from rocketdoo.project_info import project_exists, read_docker_compose
30
+
31
+ console = Console()
32
+
33
+
34
+ # ─────────────────────────────────────────────────────────────
35
+ # Internal helpers
36
+ # ─────────────────────────────────────────────────────────────
37
+
38
+ def _load_shared_meta(project_dir: Path) -> dict | None:
39
+ """Loads the rkd-shared.json file if it exists."""
40
+ meta_path = project_dir / "rkd-shared.json"
41
+ if meta_path.exists():
42
+ try:
43
+ return json.loads(meta_path.read_text())
44
+ except Exception:
45
+ return None
46
+ return None
47
+
48
+
49
+ def _find_backup_files(project_dir: Path) -> tuple[Path | None, Path | None]:
50
+ """
51
+ Searches for backup files inside the rkd_backups directory.
52
+ Returns (db_dump_path, filestore_tar_path) — None if not found.
53
+ """
54
+ backup_dir = project_dir / "rkd_backups"
55
+ if not backup_dir.exists():
56
+ return None, None
57
+
58
+ dumps = sorted(backup_dir.glob("db_*.dump"), reverse=True)
59
+ filestores = sorted(backup_dir.glob("filestore_*.tar.gz"), reverse=True)
60
+
61
+ return (dumps[0] if dumps else None), (filestores[0] if filestores else None)
62
+
63
+
64
+ def _check_ports(meta: dict) -> tuple[int, int, bool]:
65
+ """
66
+ Verifies whether the environment's ports are available.
67
+ Returns (final_odoo_port, final_vsc_port, had_changes).
68
+ """
69
+ odoo_port = int(meta.get("odoo_port", 8069))
70
+ vsc_port = int(meta.get("vsc_port", 8888))
71
+ changed = False
72
+
73
+ console.print("[bold]🔍 Checking port availability...[/bold]")
74
+
75
+ if is_port_in_use(odoo_port):
76
+ suggested = find_available_port(odoo_port + 1)
77
+ console.print(f" [yellow]⚠[/yellow] Odoo port [cyan]{odoo_port}[/cyan] is already in use.")
78
+ console.print(f" [dim]Suggested port: [green]{suggested}[/green][/dim]")
79
+ use_suggested = questionary.confirm(
80
+ f"Use port {suggested} for Odoo instead of {odoo_port}?", default=True
81
+ ).ask()
82
+ odoo_port = suggested if use_suggested else click.prompt("Enter Odoo port to use", type=int, default=suggested)
83
+ changed = True
84
+ else:
85
+ console.print(f" [green]✓[/green] Odoo port [cyan]{odoo_port}[/cyan] is available.")
86
+
87
+ if is_port_in_use(vsc_port):
88
+ suggested_vsc = find_available_port(vsc_port + 1)
89
+ console.print(f" [yellow]⚠[/yellow] VSCode port [cyan]{vsc_port}[/cyan] is already in use.")
90
+ console.print(f" [dim]Suggested port: [green]{suggested_vsc}[/green][/dim]")
91
+ use_suggested_vsc = questionary.confirm(
92
+ f"Use port {suggested_vsc} for VSCode instead of {vsc_port}?", default=True
93
+ ).ask()
94
+ vsc_port = suggested_vsc if use_suggested_vsc else click.prompt("Enter VSCode port to use", type=int, default=suggested_vsc)
95
+ changed = True
96
+ else:
97
+ console.print(f" [green]✓[/green] VSCode port [cyan]{vsc_port}[/cyan] is available.")
98
+
99
+ return odoo_port, vsc_port, changed
100
+
101
+
102
+ def _update_ports_in_compose(project_dir: Path, new_odoo_port: int, new_vsc_port: int):
103
+ """
104
+ Updates port mappings in docker-compose.yaml using plain text replacement
105
+ to preserve the original file format.
106
+ """
107
+ compose_path = project_dir / "docker-compose.yaml"
108
+ if not compose_path.exists():
109
+ compose_path = project_dir / "docker-compose.yml"
110
+ if not compose_path.exists():
111
+ return
112
+
113
+ content = compose_path.read_text()
114
+ content = re.sub(r'"\d+:8069"', f'"{new_odoo_port}:8069"', content)
115
+ content = re.sub(r'"\d+:8888"', f'"{new_vsc_port}:8888"', content)
116
+ compose_path.write_text(content)
117
+ console.print(f" [green]✓[/green] docker-compose.yaml updated with new ports.")
118
+
119
+
120
+ def _configure_ssh(project_dir: Path, meta: dict) -> bool:
121
+ """
122
+ Guides the recipient through configuring their own SSH key for the environment.
123
+ Returns True if configured successfully, False if skipped or failed.
124
+ """
125
+ console.print()
126
+ console.print("[bold]🔐 SSH configuration for private repositories:[/bold]")
127
+
128
+ original_key = meta.get("ssh_key_name")
129
+ if original_key:
130
+ console.print(f" [dim]The original environment used key: [yellow]{original_key}[/yellow][/dim]")
131
+
132
+ console.print()
133
+ available_keys = list_private_keys()
134
+
135
+ if not available_keys:
136
+ console.print(" [yellow]⚠[/yellow] No SSH keys found in ~/.ssh/")
137
+ console.print(" [dim]Generate one with: [cyan]ssh-keygen -t rsa -b 4096[/cyan][/dim]")
138
+ skip = questionary.confirm(
139
+ "Continue without SSH? (private repos will not work)", default=False
140
+ ).ask()
141
+ return not skip
142
+
143
+ console.print(f" [dim]Found {len(available_keys)} SSH key(s) available.[/dim]")
144
+
145
+ selected_key = questionary.select(
146
+ "Select your SSH key for private repositories:",
147
+ choices=available_keys
148
+ ).ask()
149
+
150
+ if not selected_key:
151
+ return False
152
+
153
+ try:
154
+ dockerfile_path = project_dir / "Dockerfile"
155
+ console.print(f" [dim]Copying key [cyan]{selected_key}[/cyan] to build context...[/dim]")
156
+ copy_key_to_build_context(selected_key, project_dir)
157
+
158
+ if dockerfile_path.exists():
159
+ inject_ssh_into_dockerfile(dockerfile_path, selected_key)
160
+ console.print(f" [green]✓[/green] Dockerfile configured with key [cyan]{selected_key}[/cyan]")
161
+ else:
162
+ console.print(" [yellow]⚠[/yellow] Dockerfile not found.")
163
+
164
+ return True
165
+ except Exception as e:
166
+ console.print(f" [red]✗ Error configuring SSH:[/red] {e}")
167
+ return False
168
+
169
+
170
+ def _get_db_container_name(project_dir: Path) -> str | None:
171
+ """Reads docker-compose and returns the database container name."""
172
+ compose_data = read_docker_compose()
173
+ if compose_data:
174
+ try:
175
+ return compose_data["services"]["db"]["container_name"]
176
+ except (KeyError, TypeError):
177
+ pass
178
+ return None
179
+
180
+
181
+ def _get_odoo_container_name(project_dir: Path) -> str | None:
182
+ """Reads docker-compose and returns the web container name."""
183
+ compose_data = read_docker_compose()
184
+ if compose_data:
185
+ try:
186
+ return compose_data["services"]["web"]["container_name"]
187
+ except (KeyError, TypeError):
188
+ pass
189
+ return None
190
+
191
+
192
+ def _wait_for_postgres(db_container: str, max_wait: int = 60) -> bool:
193
+ """Waits until PostgreSQL is ready to accept connections."""
194
+ console.print(f" [dim]Waiting for PostgreSQL to be ready (max {max_wait}s)...[/dim]")
195
+ for i in range(max_wait):
196
+ result = subprocess.run(
197
+ ["docker", "exec", db_container, "pg_isready", "-U", "root"],
198
+ capture_output=True
199
+ )
200
+ if result.returncode == 0:
201
+ console.print(f" [green]✓[/green] PostgreSQL is ready.")
202
+ return True
203
+ time.sleep(1)
204
+ if i % 10 == 9:
205
+ console.print(f" [dim] ...{i + 1}s[/dim]")
206
+ return False
207
+
208
+
209
+ def _wait_for_odoo_volume(odoo_container: str, max_wait: int = 90) -> bool:
210
+ """
211
+ Waits until /var/lib/odoo is mounted and accessible inside the Odoo container.
212
+
213
+ Replaces time.sleep(5): the Docker volume may take several seconds to become
214
+ available after the container starts, especially on first run. Without this
215
+ check the filestore restore would silently fail writing to a non-existent path.
216
+ """
217
+ console.print(f" [dim]Waiting for Odoo volume to be ready (max {max_wait}s)...[/dim]")
218
+ for i in range(max_wait):
219
+ result = subprocess.run(
220
+ ["docker", "exec", odoo_container, "test", "-d", "/var/lib/odoo"],
221
+ capture_output=True
222
+ )
223
+ if result.returncode == 0:
224
+ console.print(f" [green]✓[/green] Odoo volume is ready.")
225
+ return True
226
+ time.sleep(1)
227
+ if i % 15 == 14:
228
+ console.print(f" [dim] ...{i + 1}s[/dim]")
229
+ console.print(f" [yellow]⚠[/yellow] Odoo volume not ready after {max_wait}s.")
230
+ return False
231
+
232
+
233
+ def _restore_database(db_container: str, dump_path: Path) -> str | None:
234
+ """
235
+ Restores the PostgreSQL dump into the container.
236
+ Returns the restored database name, or None if it failed.
237
+ """
238
+ stem = dump_path.stem
239
+ parts = stem.split("_")
240
+ db_name = "_".join(parts[1:-2]) if len(parts) >= 4 else (parts[1] if len(parts) > 1 else "odoo_restored")
241
+
242
+ console.print(f" [dim]Restoring database [cyan]{db_name}[/cyan]...[/dim]")
243
+
244
+ try:
245
+ copy_result = subprocess.run(
246
+ ["docker", "cp", str(dump_path), f"{db_container}:/tmp/rkd_restore.dump"],
247
+ capture_output=True, text=True
248
+ )
249
+ if copy_result.returncode != 0:
250
+ console.print(f" [red]✗ Error copying dump:[/red] {copy_result.stderr}")
251
+ return None
252
+
253
+ subprocess.run(
254
+ ["docker", "exec", db_container,
255
+ "psql", "-U", "root", "-d", "postgres", "-c",
256
+ f"DROP DATABASE IF EXISTS \"{db_name}\";"],
257
+ capture_output=True
258
+ )
259
+ create_result = subprocess.run(
260
+ ["docker", "exec", db_container,
261
+ "psql", "-U", "root", "-d", "postgres", "-c",
262
+ f"CREATE DATABASE \"{db_name}\" OWNER root;"],
263
+ capture_output=True, text=True
264
+ )
265
+ if create_result.returncode != 0:
266
+ console.print(f" [red]✗ Error creating database:[/red] {create_result.stderr}")
267
+ return None
268
+
269
+ restore_result = subprocess.run(
270
+ ["docker", "exec", db_container,
271
+ "pg_restore", "-U", "root", "-d", db_name,
272
+ "--no-owner", "--role=root",
273
+ "/tmp/rkd_restore.dump"],
274
+ capture_output=True, text=True
275
+ )
276
+
277
+ # pg_restore may return warnings (returncode 1) but still succeed
278
+ if restore_result.returncode not in (0, 1):
279
+ console.print(f" [red]✗ Restore error:[/red] {restore_result.stderr[:500]}")
280
+ return None
281
+
282
+ console.print(f" [green]✓[/green] Database [cyan]{db_name}[/cyan] restored successfully.")
283
+ return db_name
284
+
285
+ except Exception as e:
286
+ console.print(f" [red]✗ Exception during restore:[/red] {e}")
287
+ return None
288
+
289
+
290
+ def _restore_filestore(odoo_container: str, filestore_tar: Path, db_name: str) -> bool:
291
+ """Restores the Odoo filestore into the web container (supports multiple layouts)."""
292
+ console.print(f" [dim]Restoring filestore for [cyan]{db_name}[/cyan]...[/dim]")
293
+
294
+ POSSIBLE_BASES = [
295
+ "/var/lib/odoo/.local/share/Odoo/filestore",
296
+ "/var/lib/odoo/filestore",
297
+ ]
298
+
299
+ try:
300
+ # ── 1. Copy tar to container ──
301
+ copy_result = subprocess.run(
302
+ ["docker", "cp", str(filestore_tar), f"{odoo_container}:/tmp/rkd_filestore.tar.gz"],
303
+ capture_output=True, text=True
304
+ )
305
+ if copy_result.returncode != 0:
306
+ console.print(f" [yellow]⚠[/yellow] Could not copy filestore: {copy_result.stderr}")
307
+ return False
308
+
309
+ # ── 2. Detect filestore base ──
310
+ filestore_base = None
311
+
312
+ for base in POSSIBLE_BASES:
313
+ check = subprocess.run(
314
+ ["docker", "exec", odoo_container, "test", "-d", base],
315
+ capture_output=True
316
+ )
317
+ if check.returncode == 0:
318
+ filestore_base = base
319
+ break
320
+
321
+ # fallback → usar layout simple
322
+ if not filestore_base:
323
+ filestore_base = "/var/lib/odoo/filestore"
324
+ console.print(
325
+ f" [yellow]⚠[/yellow] Filestore base not found. Using fallback: "
326
+ f"[cyan]{filestore_base}[/cyan]"
327
+ )
328
+
329
+ console.print(f" [dim]Using filestore base:[/dim] [cyan]{filestore_base}[/cyan]")
330
+
331
+ # ── 3. Ensure base exists ──
332
+ subprocess.run(
333
+ ["docker", "exec", odoo_container, "mkdir", "-p", filestore_base],
334
+ capture_output=True
335
+ )
336
+
337
+ # ── 4. Clean existing filestore (VERY IMPORTANT) ──
338
+ console.print(f" [dim]Cleaning existing filestore for {db_name}...[/dim]")
339
+ subprocess.run(
340
+ ["docker", "exec", odoo_container, "rm", "-rf", f"{filestore_base}/{db_name}"],
341
+ capture_output=True
342
+ )
343
+
344
+ # ── 5. Extract tar ──
345
+ extract_result = subprocess.run(
346
+ [
347
+ "docker", "exec", odoo_container,
348
+ "tar", "-xzf", "/tmp/rkd_filestore.tar.gz",
349
+ "-C", filestore_base
350
+ ],
351
+ capture_output=True, text=True
352
+ )
353
+
354
+ if extract_result.returncode != 0:
355
+ console.print(f" [yellow]⚠[/yellow] Error extracting filestore: {extract_result.stderr}")
356
+ return False
357
+
358
+ # ── 6. Fix permissions ──
359
+ subprocess.run(
360
+ [
361
+ "docker", "exec", odoo_container,
362
+ "chown", "-R", "odoo:odoo", f"{filestore_base}/{db_name}"
363
+ ],
364
+ capture_output=True
365
+ )
366
+
367
+ console.print(f" [green]✓[/green] Filestore restored successfully.")
368
+ return True
369
+
370
+ except Exception as e:
371
+ console.print(f" [yellow]⚠[/yellow] Exception while restoring filestore: {e}")
372
+ return False
373
+
374
+
375
+ def _launch_environment(build: bool = False):
376
+ """Runs docker compose up -d (with optional --build flag)."""
377
+ cmd = ["docker", "compose", "up", "-d"]
378
+ if build:
379
+ cmd.append("--build")
380
+ console.print()
381
+ console.print(f"[bold]🚀 Starting environment:[/bold] [dim]{' '.join(cmd)}[/dim]")
382
+ result = subprocess.run(cmd)
383
+ return result.returncode == 0
384
+
385
+
386
+ def _launch_db_only():
387
+ """Starts only the db service to allow database restoration."""
388
+ console.print("[dim] Starting database service only...[/dim]")
389
+ result = subprocess.run(
390
+ ["docker", "compose", "up", "-d", "db"],
391
+ capture_output=True, text=True
392
+ )
393
+ return result.returncode == 0
394
+
395
+
396
+ # ─────────────────────────────────────────────────────────────
397
+ # Main command
398
+ # ─────────────────────────────────────────────────────────────
399
+
400
+ @click.command(name="unpack")
401
+ @click.option("--no-restore", is_flag=True, default=False,
402
+ help="Skip automatic database restoration.")
403
+ @click.option("--build", is_flag=True, default=False,
404
+ help="Rebuild the Docker image before starting (recommended on first run).")
405
+ def unpack_environment(no_restore, build):
406
+ """
407
+ 📥 Start a development environment shared by another developer.
408
+
409
+ Run this inside the unzipped environment directory.
410
+ Automatically detects shared environments, validates ports,
411
+ configures your own SSH keys, and restores the database.
412
+
413
+ \b
414
+ Examples:
415
+ rkd unpack → full setup (recommended)
416
+ rkd unpack --no-restore → skip DB restore
417
+ rkd unpack --build → rebuild Docker image
418
+ """
419
+ console.print()
420
+ console.print(Panel(
421
+ "[bold cyan]📥 RKD Unpack — Start shared environment[/bold cyan]\n\n"
422
+ "[dim]This process will:[/dim]\n"
423
+ " [green]✓[/green] Detect and validate the shared environment\n"
424
+ " [green]✓[/green] Check port availability\n"
425
+ " [green]✓[/green] Configure your SSH key if using private repos\n"
426
+ " [green]✓[/green] Start the environment and restore the database",
427
+ border_style="cyan",
428
+ box=box.ROUNDED
429
+ ))
430
+ console.print()
431
+
432
+ project_dir = Path.cwd()
433
+
434
+ # ── 1. Detect project and metadata ──
435
+ if not project_exists():
436
+ console.print("[red]✗[/red] No Rocketdoo project found in this directory.")
437
+ console.print("[dim]Make sure you are inside the unzipped environment directory.[/dim]")
438
+ return
439
+
440
+ meta = _load_shared_meta(project_dir)
441
+
442
+ if meta and meta.get("rkd_shared"):
443
+ console.print("[green]✓[/green] Shared environment detected ([dim]rkd-shared.json[/dim])")
444
+ console.print()
445
+
446
+ info_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 2))
447
+ info_table.add_column("", style="cyan bold", width=22)
448
+ info_table.add_column("", style="green")
449
+ info_table.add_row("📦 Project", meta.get("project_name", "unknown"))
450
+ info_table.add_row("🐳 Odoo", f"{meta.get('odoo_version', '?')} ({meta.get('odoo_edition', 'Community')})")
451
+ info_table.add_row("🗄️ PostgreSQL", str(meta.get("db_version", "?")))
452
+ info_table.add_row("🔐 Private repos", "Yes" if meta.get("uses_private_repos") else "No")
453
+ info_table.add_row("💾 DB Backup", "Included ✓" if meta.get("has_db_backup") else "Not included")
454
+ console.print(Panel(info_table, title="[bold]📋 Environment to start[/bold]",
455
+ border_style="dim", box=box.ROUNDED))
456
+ console.print()
457
+ else:
458
+ console.print("[yellow]⚠[/yellow] rkd-shared.json not found.")
459
+ console.print("[dim]This looks like a Rocketdoo project but was not packaged with [cyan]rkd pack[/cyan].[/dim]")
460
+ if not questionary.confirm("Continue anyway?", default=False).ask():
461
+ return
462
+ meta = {}
463
+
464
+ # ── 2. Check and adjust ports ──
465
+ console.print()
466
+ new_odoo_port, new_vsc_port, ports_changed = _check_ports(meta)
467
+
468
+ if ports_changed:
469
+ console.print()
470
+ console.print("[dim] Updating docker-compose.yaml with new ports...[/dim]")
471
+ _update_ports_in_compose(project_dir, new_odoo_port, new_vsc_port)
472
+
473
+ # ── 3. Configure SSH if the environment used private repos ──
474
+ uses_private_repos = meta.get("uses_private_repos", False)
475
+
476
+ if uses_private_repos:
477
+ console.print()
478
+ console.print(Panel(
479
+ "This environment was set up with [bold]private repositories[/bold].\n"
480
+ "You need to configure [bold]your own SSH key[/bold] for it to work correctly.",
481
+ border_style="yellow",
482
+ box=box.ROUNDED
483
+ ))
484
+ wants_ssh = questionary.confirm(
485
+ "Do you use private repositories and want to configure your SSH key?", default=True
486
+ ).ask()
487
+
488
+ if wants_ssh:
489
+ ssh_ok = _configure_ssh(project_dir, meta)
490
+ if not ssh_ok:
491
+ console.print("[yellow]⚠[/yellow] SSH not configured. Private repos may not work.")
492
+
493
+ elif not meta:
494
+ wants_ssh = questionary.confirm(
495
+ "Does this environment use private repositories? (requires SSH key)", default=False
496
+ ).ask()
497
+ if wants_ssh:
498
+ _configure_ssh(project_dir, {})
499
+
500
+ # ── 4. Locate backup files ──
501
+ db_dump, filestore_tar = _find_backup_files(project_dir)
502
+ has_backup = db_dump is not None
503
+
504
+ # ── 5. Start the environment ──
505
+ console.print()
506
+ if has_backup and not no_restore:
507
+ console.print("[bold]💾 Database backup found.[/bold]")
508
+ console.print("[dim] Strategy: start DB → restore → start full environment[/dim]")
509
+ console.print()
510
+
511
+ db_up = _launch_db_only()
512
+ if not db_up:
513
+ console.print("[red]✗ Could not start the database service.[/red]")
514
+ return
515
+
516
+ db_container = _get_db_container_name(project_dir)
517
+ if db_container:
518
+ pg_ready = _wait_for_postgres(db_container)
519
+ if pg_ready:
520
+ console.print()
521
+ console.print("[bold]💾 Restoring database:[/bold]")
522
+ restored_db = _restore_database(db_container, db_dump)
523
+
524
+ if restored_db and filestore_tar:
525
+ console.print()
526
+ console.print("[bold]🗂️ Restoring filestore:[/bold]")
527
+ console.print("[dim] Starting web service to restore filestore...[/dim]")
528
+ subprocess.run(["docker", "compose", "up", "-d", "web"], capture_output=True)
529
+
530
+ odoo_container = _get_odoo_container_name(project_dir)
531
+ if odoo_container:
532
+ volume_ready = _wait_for_odoo_volume(odoo_container)
533
+ if volume_ready:
534
+ _restore_filestore(odoo_container, filestore_tar, restored_db)
535
+ else:
536
+ console.print(
537
+ "[yellow]⚠[/yellow] Skipping filestore restore: "
538
+ "Odoo volume was not ready in time.\n"
539
+ "[dim] Try running the restore manually after the environment is up.[/dim]"
540
+ )
541
+
542
+ _launch_environment(build=build)
543
+ else:
544
+ _launch_environment(build=build)
545
+
546
+ # ── 6. Final summary ──
547
+ console.print()
548
+ console.print(Panel(
549
+ f"[bold green]✅ Environment is ready[/bold green]\n\n"
550
+ f"[bold]🌐 Odoo:[/bold] [cyan underline]http://localhost:{new_odoo_port}[/cyan underline]\n"
551
+ f"[bold]🐛 Debug:[/bold] port [cyan]{new_vsc_port}[/cyan]\n\n"
552
+ f"[dim]Useful commands:\n"
553
+ f" [cyan]rkd status[/cyan] → check container status\n"
554
+ f" [cyan]rkd logs[/cyan] → view logs\n"
555
+ f" [cyan]rkd info[/cyan] → project information[/dim]",
556
+ border_style="green",
557
+ box=box.ROUNDED
558
+ ))
559
+ console.print()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocketdoo
3
- Version: 2.2.4
3
+ Version: 2.3.1
4
4
  Summary: Framework for creating Odoo development environments with Docker and custom templates.
5
5
  Author-email: Horacio Montaño <horaciomontano@hdmsoft.com.ar>
6
6
  License: GNU LESSER GENERAL PUBLIC LICENSE
@@ -62,7 +62,7 @@ License: GNU LESSER GENERAL PUBLIC LICENSE
62
62
  <div RKD as ROCKETDOO V2=""></div>
63
63
 
64
64
  Licencia: LGPL-3.0+
65
- Versión: "2.2.4"
65
+ Versión: "2.3.1"
66
66
  Autor: Horacio Montaño, Elias Braceras
67
67
  Fecha: 16/10/2024
68
68
  Descripción: Framework to development Odoo
@@ -95,7 +95,7 @@ Odoo Development Framework
95
95
  - "Horacio Montaño" and "Elias Braceras"
96
96
 
97
97
  ## Version:
98
- - "2.2.4"
98
+ - "2.3.1"
99
99
 
100
100
  ----------------------------------------------------------------------------------------------------------------------------------------------------------
101
101
 
@@ -10,9 +10,11 @@ rocketdoo/deploy_cli.py
10
10
  rocketdoo/docker_cli.py
11
11
  rocketdoo/init_project.py
12
12
  rocketdoo/main.py
13
+ rocketdoo/pack_environment.py
13
14
  rocketdoo/project_info.py
14
15
  rocketdoo/rocketdoo.py
15
16
  rocketdoo/scaffold.py
17
+ rocketdoo/unpack_environment.py
16
18
  rocketdoo/welcome.py
17
19
  rocketdoo.egg-info/PKG-INFO
18
20
  rocketdoo.egg-info/SOURCES.txt
File without changes
File without changes
File without changes
File without changes