ots-containers 0.3.0__py3-none-any.whl

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 (37) hide show
  1. ots_containers/__init__.py +3 -0
  2. ots_containers/assets.py +32 -0
  3. ots_containers/cli.py +86 -0
  4. ots_containers/commands/README.md +124 -0
  5. ots_containers/commands/__init__.py +9 -0
  6. ots_containers/commands/assets.py +29 -0
  7. ots_containers/commands/cloudinit/__init__.py +6 -0
  8. ots_containers/commands/cloudinit/app.py +158 -0
  9. ots_containers/commands/cloudinit/templates.py +127 -0
  10. ots_containers/commands/common.py +71 -0
  11. ots_containers/commands/env/__init__.py +6 -0
  12. ots_containers/commands/env/app.py +348 -0
  13. ots_containers/commands/image/__init__.py +6 -0
  14. ots_containers/commands/image/app.py +1137 -0
  15. ots_containers/commands/init.py +253 -0
  16. ots_containers/commands/instance/__init__.py +57 -0
  17. ots_containers/commands/instance/_helpers.py +161 -0
  18. ots_containers/commands/instance/annotations.py +111 -0
  19. ots_containers/commands/instance/app.py +935 -0
  20. ots_containers/commands/proxy/__init__.py +11 -0
  21. ots_containers/commands/proxy/_helpers.py +95 -0
  22. ots_containers/commands/proxy/app.py +160 -0
  23. ots_containers/commands/service/__init__.py +6 -0
  24. ots_containers/commands/service/_helpers.py +294 -0
  25. ots_containers/commands/service/app.py +498 -0
  26. ots_containers/commands/service/packages.py +217 -0
  27. ots_containers/config.py +158 -0
  28. ots_containers/db.py +582 -0
  29. ots_containers/environment_file.py +491 -0
  30. ots_containers/podman.py +60 -0
  31. ots_containers/quadlet.py +341 -0
  32. ots_containers/systemd.py +217 -0
  33. ots_containers-0.3.0.dist-info/METADATA +284 -0
  34. ots_containers-0.3.0.dist-info/RECORD +37 -0
  35. ots_containers-0.3.0.dist-info/WHEEL +4 -0
  36. ots_containers-0.3.0.dist-info/entry_points.txt +2 -0
  37. ots_containers-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ # src/ots_containers/__init__.py
2
+
3
+ __version__ = "0.2.2"
@@ -0,0 +1,32 @@
1
+ # src/ots_containers/assets.py
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .config import Config
7
+ from .podman import podman
8
+
9
+
10
+ def update(cfg: Config, create_volume: bool = True) -> None:
11
+ if create_volume:
12
+ podman.volume.create("static_assets", check=False)
13
+
14
+ try:
15
+ result = podman.volume.mount("static_assets", capture_output=True, text=True, check=True)
16
+ except subprocess.CalledProcessError as e:
17
+ stderr = e.stderr.strip() if e.stderr else "unknown error"
18
+ raise SystemExit(f"Failed to mount volume 'static_assets': {stderr}")
19
+ assets_dir = Path(result.stdout.strip())
20
+
21
+ result = podman.create(cfg.resolved_image_with_tag, capture_output=True, text=True, check=True)
22
+ container_id = result.stdout.strip()
23
+
24
+ try:
25
+ podman.cp(f"{container_id}:/app/public/.", str(assets_dir), check=True)
26
+ manifest = assets_dir / "web/dist/.vite/manifest.json"
27
+ if manifest.exists():
28
+ print(f"Manifest found: {manifest}")
29
+ else:
30
+ print(f"Warning: manifest not found at {manifest}")
31
+ finally:
32
+ podman.rm(container_id, check=True)
ots_containers/cli.py ADDED
@@ -0,0 +1,86 @@
1
+ # src/ots_containers/cli.py
2
+
3
+ """
4
+ Manage OTS Podman containers via Quadlets.
5
+
6
+ Usage:
7
+
8
+ ots-containers init
9
+ ots-containers image pull --tag v0.23.0 --current
10
+ ots-containers instance deploy 7043
11
+ ots-containers instance redeploy 7044
12
+ ots-containers image rollback
13
+ ots-containers instance redeploy
14
+ ots-containers assets sync
15
+
16
+ # Or run directly without installing:
17
+ $ cd src/
18
+ $ pip install -e .
19
+ $ python -m ots_containers.cli instance deploy 7043
20
+ """
21
+
22
+ import cyclopts
23
+
24
+ from . import __version__
25
+ from .commands import assets as assets_cmd
26
+ from .commands import cloudinit, env, image, init, instance, proxy, service
27
+ from .podman import podman
28
+
29
+ app = cyclopts.App(
30
+ name="ots-containers",
31
+ help="Service orchestration for OTS: Podman Quadlets and systemd services",
32
+ version=__version__,
33
+ )
34
+
35
+ # Register topic sub-apps
36
+ app.command(init.app)
37
+ app.command(instance.app)
38
+ app.command(image.app)
39
+ app.command(assets_cmd.app)
40
+ app.command(proxy.app)
41
+ app.command(service.app)
42
+ app.command(cloudinit.app)
43
+ app.command(env.app)
44
+
45
+
46
+ @app.default
47
+ def _default():
48
+ """Show help when no command is specified."""
49
+ app.help_print([])
50
+
51
+
52
+ # Root-level command for quick access
53
+ @app.command
54
+ def ps():
55
+ """Show running OTS containers (podman view)."""
56
+ podman.ps(
57
+ filter="name=onetime",
58
+ format="table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}",
59
+ )
60
+
61
+
62
+ @app.command
63
+ def version():
64
+ """Show version and build info."""
65
+ import subprocess
66
+ from pathlib import Path
67
+
68
+ print(f"ots-containers {__version__}")
69
+
70
+ # Try to get git info if available
71
+ try:
72
+ pkg_dir = Path(__file__).parent
73
+ result = subprocess.run(
74
+ ["git", "-C", str(pkg_dir), "rev-parse", "--short", "HEAD"],
75
+ capture_output=True,
76
+ text=True,
77
+ )
78
+ if result.returncode == 0:
79
+ commit = result.stdout.strip()
80
+ print(f"git commit: {commit}")
81
+ except Exception:
82
+ pass
83
+
84
+
85
+ if __name__ == "__main__":
86
+ app()
@@ -0,0 +1,124 @@
1
+ # CLI Style Guide
2
+
3
+ ## Command Structure
4
+
5
+ We follow a Heroku-style `topic:command` pattern:
6
+
7
+ ```
8
+ ots-containers <topic> <command> [identifiers] [flags]
9
+ ```
10
+
11
+ ## Instance Types
12
+
13
+ Three container types, each with explicit naming:
14
+
15
+ | Type | Systemd Unit | Identifier | Use |
16
+ |------|--------------|------------|-----|
17
+ | `web` | `onetime-web@{port}` | Port number | HTTP servers |
18
+ | `worker` | `onetime-worker@{id}` | Name or number | Background jobs |
19
+ | `scheduler` | `onetime-scheduler@{id}` | Name or number | Scheduled tasks |
20
+
21
+ ## Command Syntax
22
+
23
+ ```bash
24
+ # Positional identifiers with type flag
25
+ ots instances restart --web 7043 7044
26
+ ots instances restart --worker billing emails
27
+ ots instances restart --scheduler main
28
+
29
+ # Auto-discover all types when no args
30
+ ots instances stop # stops ALL running instances
31
+ ots instances status # shows ALL configured instances
32
+
33
+ # Type-specific discovery
34
+ ots instances status --web # only web instances
35
+ ots instances logs --scheduler -f # only scheduler logs
36
+ ```
37
+
38
+ ## Topics
39
+
40
+ Each topic is a separate module with its own `cyclopts.App`:
41
+
42
+ | Topic | Purpose |
43
+ |-------|---------|
44
+ | `instance` | Container lifecycle and runtime control |
45
+ | `service` | Native systemd services (Valkey, Redis) |
46
+ | `image` | Container image management |
47
+ | `assets` | Static asset management |
48
+ | `cloudinit` | Cloud-init configuration generation |
49
+ | `env` | Environment file management |
50
+
51
+ To add a new topic, create a module and register it in `cli.py`.
52
+
53
+ ## Two-Level Abstraction
54
+
55
+ Commands are categorized by their impact:
56
+
57
+ ### High-level (affects config + state)
58
+ Commands that modify quadlet templates, database records, or both:
59
+ - `deploy`, `redeploy`, `undeploy`
60
+
61
+ These commands should document their config impact in the docstring.
62
+
63
+ ### Low-level (runtime control only)
64
+ Commands that only interact with systemd, no config changes:
65
+ - `start`, `stop`, `restart`, `status`, `logs`, `enable`, `disable`, `exec`
66
+
67
+ These commands should explicitly state they do NOT refresh config.
68
+
69
+ ## Naming Conventions
70
+
71
+ | Pattern | Example | Use for |
72
+ |---------|---------|---------|
73
+ | Verb | `deploy`, `sync` | Actions |
74
+ | `--flag` | `--force`, `--yes` | Boolean options |
75
+ | `--option VALUE` | `--delay 5`, `--lines 50` | Value options |
76
+ | `--type` shortcuts | `--web`, `--worker`, `--scheduler` | Instance type selection |
77
+
78
+ ## Default Commands
79
+
80
+ Use `@app.default` for the "list" operation when invoking a topic without a subcommand:
81
+
82
+ ```python
83
+ @app.default
84
+ def list_instances(
85
+ identifiers: Identifiers = (),
86
+ web: WebFlag = False,
87
+ worker: WorkerFlag = False,
88
+ scheduler: SchedulerFlag = False,
89
+ ):
90
+ """List instances with status and deployment info."""
91
+ ...
92
+ ```
93
+
94
+ This follows Heroku's pattern where `heroku apps` lists apps.
95
+
96
+ ## Help Text
97
+
98
+ First line: Brief imperative description.
99
+ Blank line, then: Config impact and usage notes.
100
+ Include Examples section with common use cases:
101
+
102
+ ```python
103
+ @app.command
104
+ def redeploy(...):
105
+ """Regenerate quadlet and restart containers.
106
+
107
+ Rewrites quadlet config, restarts service. Records to timeline for audit.
108
+ Use --force to fully teardown and recreate.
109
+
110
+ Examples:
111
+ ots instances redeploy # Redeploy all running
112
+ ots instances redeploy --web # Redeploy web instances
113
+ ots instances redeploy --web 7043 7044 # Redeploy specific web
114
+ ots instances redeploy --scheduler main # Redeploy specific scheduler
115
+ """
116
+ ```
117
+
118
+ ## Adding Commands
119
+
120
+ 1. Add to existing topic module, or create new topic
121
+ 2. Use shared helpers from `_helpers.py` (`resolve_identifiers`, `for_each_instance`)
122
+ 3. Use type annotations from `annotations.py` (`Identifiers`, `WebFlag`, etc.)
123
+ 4. Document config impact and include Examples in docstring
124
+ 5. Register new topics in `cli.py` via `app.command(topic.app)`
@@ -0,0 +1,9 @@
1
+ # src/ots_containers/commands/__init__.py
2
+ """Command topic modules for ots-containers CLI."""
3
+
4
+ from . import assets as assets
5
+ from . import image as image
6
+ from . import init as init
7
+ from . import instance as instance
8
+ from . import proxy as proxy
9
+ from . import service as service
@@ -0,0 +1,29 @@
1
+ # src/ots_containers/commands/assets.py
2
+ """Asset management commands for OTS containers."""
3
+
4
+ from typing import Annotated
5
+
6
+ import cyclopts
7
+
8
+ from .. import assets as assets_module
9
+ from ..config import Config
10
+
11
+ app = cyclopts.App(name="assets", help="Extract web assets from container image to volume")
12
+
13
+
14
+ @app.command
15
+ def sync(
16
+ create_volume: Annotated[
17
+ bool,
18
+ cyclopts.Parameter(help="Create volume if it doesn't exist (use on first deploy)"),
19
+ ] = False,
20
+ ):
21
+ """Copy /app/public from container image to static_assets podman volume.
22
+
23
+ Extracts web assets (JS, CSS, images) from the OTS container image
24
+ to a shared volume that Caddy serves directly. Use --create-volume
25
+ on initial setup.
26
+ """
27
+ cfg = Config()
28
+ cfg.validate()
29
+ assets_module.update(cfg, create_volume=create_volume)
@@ -0,0 +1,6 @@
1
+ # src/ots_containers/commands/cloudinit/__init__.py
2
+ """Cloud-init configuration generation for OTS infrastructure."""
3
+
4
+ from .app import app
5
+
6
+ __all__ = ["app"]
@@ -0,0 +1,158 @@
1
+ # src/ots_containers/commands/cloudinit/app.py
2
+ """Cloud-init configuration generation commands.
3
+
4
+ Generates cloud-init YAML with Debian 13 (Trixie) DEB822-style apt sources.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import cyclopts
12
+
13
+ from .templates import generate_cloudinit_config
14
+
15
+ app = cyclopts.App(
16
+ name="cloudinit",
17
+ help="Generate cloud-init configurations for OTS infrastructure",
18
+ )
19
+
20
+
21
+ @app.default
22
+ def _default():
23
+ """Show cloud-init commands help."""
24
+ print("Cloud-init configuration generation for OTS infrastructure")
25
+ print()
26
+ print("Supports Debian 13 (Trixie) DEB822-style apt sources")
27
+ print()
28
+ print("Use 'ots-containers cloudinit --help' for available commands")
29
+
30
+
31
+ @app.command
32
+ def generate(
33
+ output: Annotated[
34
+ str | None,
35
+ cyclopts.Parameter(help="Output file path (default: stdout)"),
36
+ ] = None,
37
+ *,
38
+ include_postgresql: Annotated[
39
+ bool, cyclopts.Parameter(help="Include PostgreSQL apt repository")
40
+ ] = False,
41
+ include_valkey: Annotated[
42
+ bool, cyclopts.Parameter(help="Include Valkey apt repository")
43
+ ] = False,
44
+ postgresql_key: Annotated[
45
+ str | None,
46
+ cyclopts.Parameter(help="Path to PostgreSQL GPG key file"),
47
+ ] = None,
48
+ valkey_key: Annotated[
49
+ str | None,
50
+ cyclopts.Parameter(help="Path to Valkey GPG key file"),
51
+ ] = None,
52
+ ):
53
+ """Generate cloud-init configuration with Debian 13 apt sources.
54
+
55
+ Generates a cloud-init YAML file with:
56
+ - Debian 13 (Trixie) main repositories in DEB822 format
57
+ - Optional PostgreSQL official repository
58
+ - Optional Valkey repository
59
+ - Package update/upgrade configuration
60
+
61
+ Example:
62
+ ots-containers cloudinit generate > user-data.yaml
63
+ ots-containers cloudinit generate --output /tmp/cloud-init.yaml
64
+ ots-containers cloudinit generate --include-postgresql --postgresql-key /path/to/pgp.asc
65
+ """
66
+ # Read GPG keys if provided
67
+ postgresql_gpg = None
68
+ valkey_gpg = None
69
+
70
+ if include_postgresql and postgresql_key:
71
+ postgresql_gpg = Path(postgresql_key).read_text()
72
+ elif include_postgresql:
73
+ print(
74
+ "Warning: --include-postgresql specified but no --postgresql-key provided",
75
+ file=sys.stderr,
76
+ )
77
+ print("PostgreSQL repository will use inline key placeholder", file=sys.stderr)
78
+
79
+ if include_valkey and valkey_key:
80
+ valkey_gpg = Path(valkey_key).read_text()
81
+ elif include_valkey:
82
+ print(
83
+ "Warning: --include-valkey specified but no --valkey-key provided",
84
+ file=sys.stderr,
85
+ )
86
+ print("Valkey repository will use inline key placeholder", file=sys.stderr)
87
+
88
+ # Generate configuration
89
+ config = generate_cloudinit_config(
90
+ include_postgresql=include_postgresql,
91
+ include_valkey=include_valkey,
92
+ postgresql_gpg_key=postgresql_gpg,
93
+ valkey_gpg_key=valkey_gpg,
94
+ )
95
+
96
+ # Output
97
+ if output:
98
+ Path(output).write_text(config)
99
+ print(f"[created] {output}")
100
+ else:
101
+ print(config)
102
+
103
+
104
+ @app.command
105
+ def validate(
106
+ file_path: Annotated[
107
+ str,
108
+ cyclopts.Parameter(help="Path to cloud-init YAML file"),
109
+ ],
110
+ ):
111
+ """Validate a cloud-init configuration file.
112
+
113
+ Checks for:
114
+ - Valid YAML syntax
115
+ - Required cloud-init sections
116
+ - DEB822 apt sources format
117
+
118
+ Example:
119
+ ots-containers cloudinit validate user-data.yaml
120
+ """
121
+ import yaml
122
+
123
+ try:
124
+ config_path = Path(file_path)
125
+ if not config_path.exists():
126
+ print(f"[error] File not found: {file_path}", file=sys.stderr)
127
+ sys.exit(1)
128
+
129
+ content = config_path.read_text()
130
+ data = yaml.safe_load(content)
131
+
132
+ # Basic validation
133
+ errors = []
134
+
135
+ if not isinstance(data, dict):
136
+ errors.append("Root element must be a dictionary")
137
+
138
+ if "apt" in data and "sources_list" in data["apt"]:
139
+ sources_list = data["apt"]["sources_list"]
140
+ # Check for DEB822 format indicators
141
+ if "Types:" not in sources_list and "URIs:" not in sources_list:
142
+ errors.append("apt.sources_list doesn't appear to use DEB822 format")
143
+
144
+ if errors:
145
+ print(f"[error] Validation failed for {file_path}:", file=sys.stderr)
146
+ for error in errors:
147
+ print(f" - {error}", file=sys.stderr)
148
+ sys.exit(1)
149
+ else:
150
+ print(f"[ok] {file_path} is valid")
151
+
152
+ except yaml.YAMLError as e:
153
+ print(f"[error] Invalid YAML in {file_path}:", file=sys.stderr)
154
+ print(f" {e}", file=sys.stderr)
155
+ sys.exit(1)
156
+ except Exception as e:
157
+ print(f"[error] Validation failed: {e}", file=sys.stderr)
158
+ sys.exit(1)
@@ -0,0 +1,127 @@
1
+ # src/ots_containers/commands/cloudinit/templates.py
2
+ """Cloud-init configuration templates with Debian 13 DEB822 apt sources."""
3
+
4
+
5
+ def get_debian13_sources_list() -> str:
6
+ """Get just the Debian 13 DEB822 sources.list content.
7
+
8
+ Returns:
9
+ DEB822-formatted sources.list content
10
+ """
11
+ return """Types: deb
12
+ URIs: http://deb.debian.org/debian
13
+ Suites: trixie trixie-updates
14
+ Components: main contrib non-free non-free-firmware
15
+ Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
16
+
17
+ Types: deb
18
+ URIs: http://deb.debian.org/debian
19
+ Suites: trixie-backports
20
+ Components: main contrib non-free non-free-firmware
21
+ Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
22
+
23
+ Types: deb
24
+ URIs: http://security.debian.org/debian-security
25
+ Suites: trixie-security
26
+ Components: main contrib non-free non-free-firmware
27
+ Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
28
+ """
29
+
30
+
31
+ def generate_cloudinit_config(
32
+ *,
33
+ include_postgresql: bool = False,
34
+ include_valkey: bool = False,
35
+ postgresql_gpg_key: str | None = None,
36
+ valkey_gpg_key: str | None = None,
37
+ ) -> str:
38
+ """Generate cloud-init YAML with Debian 13 DEB822-style apt sources.
39
+
40
+ Args:
41
+ include_postgresql: Include PostgreSQL official repository
42
+ include_valkey: Include Valkey repository
43
+ postgresql_gpg_key: PostgreSQL GPG public key content
44
+ valkey_gpg_key: Valkey GPG public key content
45
+
46
+ Returns:
47
+ Complete cloud-init YAML configuration as string
48
+ """
49
+ # Base configuration with Debian 13 main repositories
50
+ # Get the sources list and indent it for YAML
51
+ sources_list = get_debian13_sources_list().rstrip("\n")
52
+ indented_sources = "\n".join(f" {line}" for line in sources_list.split("\n"))
53
+
54
+ config_parts = [
55
+ "#cloud-config",
56
+ "# Generated cloud-init configuration for OTS infrastructure",
57
+ "# Debian 13 (Trixie) with DEB822-style apt sources",
58
+ "",
59
+ "package_update: true",
60
+ "package_upgrade: true",
61
+ "package_reboot_if_required: true",
62
+ "",
63
+ "apt:",
64
+ " sources_list: |",
65
+ indented_sources,
66
+ ]
67
+
68
+ # Add third-party repositories if requested
69
+ sources = []
70
+
71
+ if include_postgresql:
72
+ postgresql_source = {
73
+ "source": "deb http://apt.postgresql.org/pub/repos/apt trixie-pgdg main",
74
+ }
75
+ if postgresql_gpg_key:
76
+ postgresql_source["key"] = postgresql_gpg_key
77
+ else:
78
+ postgresql_source["key"] = "# PostgreSQL GPG key placeholder - replace with actual key"
79
+ sources.append(("postgresql", postgresql_source))
80
+
81
+ if include_valkey:
82
+ valkey_source = {
83
+ "source": "deb https://packages.valkey.io/deb/ trixie main",
84
+ }
85
+ if valkey_gpg_key:
86
+ valkey_source["key"] = valkey_gpg_key
87
+ else:
88
+ valkey_source["key"] = "# Valkey GPG key placeholder - replace with actual key"
89
+ sources.append(("valkey", valkey_source))
90
+
91
+ # Add sources section if we have any
92
+ if sources:
93
+ config_parts.append(" sources:")
94
+ for name, source_config in sources:
95
+ config_parts.append(f" {name}:")
96
+ config_parts.append(f' source: "{source_config["source"]}"')
97
+ if "key" in source_config:
98
+ # Multi-line key handling
99
+ key_content = source_config["key"]
100
+ if "\n" in key_content:
101
+ config_parts.append(" key: |")
102
+ for line in key_content.split("\n"):
103
+ config_parts.append(f" {line}")
104
+ else:
105
+ config_parts.append(f' key: "{key_content}"')
106
+
107
+ # Add common packages section
108
+ config_parts.extend(
109
+ [
110
+ "",
111
+ "packages:",
112
+ " - curl",
113
+ " - wget",
114
+ " - git",
115
+ " - vim",
116
+ " - podman",
117
+ " - systemd-container",
118
+ ]
119
+ )
120
+
121
+ if include_postgresql:
122
+ config_parts.append(" - postgresql-client")
123
+
124
+ if include_valkey:
125
+ config_parts.append(" - valkey")
126
+
127
+ return "\n".join(config_parts) + "\n"
@@ -0,0 +1,71 @@
1
+ # src/ots_containers/commands/common.py
2
+ """Shared CLI annotations for consistency across commands.
3
+
4
+ All common flags use long+short forms for consistency:
5
+ --quiet, -q
6
+ --dry-run, -n
7
+ --yes, -y
8
+ --follow, -f
9
+ --lines, -n
10
+ --json, -j
11
+ """
12
+
13
+ from typing import Annotated
14
+
15
+ import cyclopts
16
+
17
+ # Output control
18
+ Quiet = Annotated[
19
+ bool,
20
+ cyclopts.Parameter(
21
+ name=["--quiet", "-q"],
22
+ help="Suppress output",
23
+ ),
24
+ ]
25
+
26
+ DryRun = Annotated[
27
+ bool,
28
+ cyclopts.Parameter(
29
+ name=["--dry-run", "-n"],
30
+ help="Show what would be done without doing it",
31
+ negative=[], # Disable --no-dry-run generation
32
+ ),
33
+ ]
34
+
35
+
36
+ # Confirmation
37
+ Yes = Annotated[
38
+ bool,
39
+ cyclopts.Parameter(
40
+ name=["--yes", "-y"],
41
+ help="Skip confirmation prompts",
42
+ ),
43
+ ]
44
+
45
+
46
+ # Log viewing
47
+ Follow = Annotated[
48
+ bool,
49
+ cyclopts.Parameter(
50
+ name=["--follow", "-f"],
51
+ help="Follow log output",
52
+ ),
53
+ ]
54
+
55
+ Lines = Annotated[
56
+ int,
57
+ cyclopts.Parameter(
58
+ name=["--lines", "-n"],
59
+ help="Number of lines to show",
60
+ ),
61
+ ]
62
+
63
+
64
+ # JSON output
65
+ JsonOutput = Annotated[
66
+ bool,
67
+ cyclopts.Parameter(
68
+ name=["--json", "-j"],
69
+ help="Output as JSON",
70
+ ),
71
+ ]
@@ -0,0 +1,6 @@
1
+ # src/ots_containers/commands/env/__init__.py
2
+ """Environment file management commands."""
3
+
4
+ from .app import app
5
+
6
+ __all__ = ["app"]