flowstash-cli 0.1.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.
- flowstash/__init__.py +0 -0
- flowstash/commands/__init__.py +0 -0
- flowstash/commands/auth.py +112 -0
- flowstash/commands/build.py +127 -0
- flowstash/commands/deploy.py +143 -0
- flowstash/commands/project.py +555 -0
- flowstash/commands/run.py +65 -0
- flowstash/core/__init__.py +0 -0
- flowstash/core/api_client.py +62 -0
- flowstash/core/auth_server.py +45 -0
- flowstash/core/builder.py +40 -0
- flowstash/core/config.py +81 -0
- flowstash/core/docker_utils.py +33 -0
- flowstash/main.py +269 -0
- flowstash/templates/AGENTS.md +222 -0
- flowstash/templates/README.md +135 -0
- flowstash/templates/_.dockerignore +8 -0
- flowstash/templates/_.flowstash +4 -0
- flowstash/templates/_api_main.py +21 -0
- flowstash/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
- flowstash/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
- flowstash/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
- flowstash/templates/_config/[env]/_backend.yaml +4 -0
- flowstash/templates/_config/shared/.env +1 -0
- flowstash/templates/_config/shared/backend.yaml +4 -0
- flowstash/templates/_config/shared/clients/demoClient.yaml +39 -0
- flowstash/templates/_config/shared/clients.yaml +3 -0
- flowstash/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +24 -0
- flowstash/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
- flowstash/templates/_deployment/[env]/.env +3 -0
- flowstash/templates/_deployment/shared/.env +5 -0
- flowstash/templates/_deployment/shared/api.Dockerfile +18 -0
- flowstash/templates/_deployment/shared/worker.Dockerfile +18 -0
- flowstash/templates/_pyproject.toml +40 -0
- flowstash/templates/_src/_api/__init__.py +1 -0
- flowstash/templates/_src/_api/_routes/webhooks.py +25 -0
- flowstash/templates/_src/_shared/__init__.py +1 -0
- flowstash/templates/_src/_shared/clients/client.py +18 -0
- flowstash/templates/_src/_shared/models/models.py +1 -0
- flowstash/templates/_src/_shared/tasks/sharedTasks.py +10 -0
- flowstash/templates/_src/_worker/__init__.py +1 -0
- flowstash/templates/_src/_worker/tasks/tasks.py +15 -0
- flowstash/templates/_worker_main.py +25 -0
- flowstash/ui/__init__.py +0 -0
- flowstash_cli-0.1.0.dist-info/METADATA +19 -0
- flowstash_cli-0.1.0.dist-info/RECORD +48 -0
- flowstash_cli-0.1.0.dist-info/WHEEL +4 -0
- flowstash_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
import threading
|
|
3
|
+
import queue
|
|
4
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
5
|
+
from urllib.parse import urlparse, parse_qs
|
|
6
|
+
|
|
7
|
+
result_queue = queue.Queue()
|
|
8
|
+
|
|
9
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
10
|
+
def do_GET(self):
|
|
11
|
+
query_components = parse_qs(urlparse(self.path).query)
|
|
12
|
+
params = {k: v[0] for k, v in query_components.items()}
|
|
13
|
+
result_queue.put(params)
|
|
14
|
+
|
|
15
|
+
self.send_response(200)
|
|
16
|
+
self.send_header('Content-type', 'text/html')
|
|
17
|
+
self.end_headers()
|
|
18
|
+
|
|
19
|
+
content = """
|
|
20
|
+
<html>
|
|
21
|
+
<body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
|
|
22
|
+
<h1>Authentication Successful</h1>
|
|
23
|
+
<p>You can now close this window and return to the CLI.</p>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
"""
|
|
27
|
+
self.wfile.write(content.encode('utf-8'))
|
|
28
|
+
|
|
29
|
+
def log_message(self, format, *args):
|
|
30
|
+
# Suppress logging
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def start_callback_server(port: int = 8888) -> Dict[str, str]:
|
|
34
|
+
server = HTTPServer(('127.0.0.1', port), CallbackHandler)
|
|
35
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
36
|
+
thread.start()
|
|
37
|
+
|
|
38
|
+
# Wait for result
|
|
39
|
+
try:
|
|
40
|
+
result = result_queue.get(timeout=300) # 5 minutes timeout
|
|
41
|
+
server.shutdown()
|
|
42
|
+
return result
|
|
43
|
+
except queue.Empty:
|
|
44
|
+
server.shutdown()
|
|
45
|
+
return {}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tarfile
|
|
3
|
+
import tempfile
|
|
4
|
+
import gzip
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
def bundle_source(directory: Path, ignore_patterns: Optional[List[str]] = None) -> Path:
|
|
9
|
+
"""
|
|
10
|
+
Bundles the source code in directory into a .tar.gz file.
|
|
11
|
+
Follows .gitignore if present.
|
|
12
|
+
"""
|
|
13
|
+
temp_dir = Path(tempfile.gettempdir())
|
|
14
|
+
tar_path = temp_dir / f"source_{os.urandom(4).hex()}.tar.gz"
|
|
15
|
+
|
|
16
|
+
# Basic ignore logic (should be improved to handle .gitignore properly)
|
|
17
|
+
default_ignore = {".git", ".venv", "__pycache__", ".pytest_cache", ".flowstash"}
|
|
18
|
+
if ignore_patterns:
|
|
19
|
+
default_ignore.update(ignore_patterns)
|
|
20
|
+
|
|
21
|
+
def is_ignored(path: Path) -> bool:
|
|
22
|
+
for part in path.parts:
|
|
23
|
+
if part in default_ignore:
|
|
24
|
+
return True
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
with tarfile.open(tar_path, "w:gz") as tar:
|
|
28
|
+
for root, dirs, files in os.walk(directory):
|
|
29
|
+
root_path = Path(root)
|
|
30
|
+
if is_ignored(root_path.relative_to(directory)):
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
for file in files:
|
|
34
|
+
file_path = root_path / file
|
|
35
|
+
if is_ignored(file_path.relative_to(directory)):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
tar.add(file_path, arcname=str(file_path.relative_to(directory)))
|
|
39
|
+
|
|
40
|
+
return tar_path
|
flowstash/core/config.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
import yaml
|
|
7
|
+
import keyring
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
flowstash_DIR = Path.home() / ".flowstash"
|
|
11
|
+
GLOBAL_CONFIG_FILE = flowstash_DIR / "config.yaml"
|
|
12
|
+
PROJECT_CONFIG_FILE = ".flowstash"
|
|
13
|
+
|
|
14
|
+
SERVICE_NAME = "flowstash"
|
|
15
|
+
TOKEN_KEY = "access_token"
|
|
16
|
+
|
|
17
|
+
class GlobalConfig(BaseModel):
|
|
18
|
+
api_url: str = os.getenv("flowstash_API_URL", "https://api.flowstash.dev") # Default to managed platform
|
|
19
|
+
|
|
20
|
+
class EnvironmentMode(BaseModel):
|
|
21
|
+
name: str
|
|
22
|
+
managed: bool = False
|
|
23
|
+
options: Dict[str, str] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
class ProjectConfig(BaseModel):
|
|
26
|
+
project_name: Optional[str] = None
|
|
27
|
+
project_id: Optional[str] = None
|
|
28
|
+
tenant_id: Optional[str] = None
|
|
29
|
+
user_email: Optional[str] = None
|
|
30
|
+
environments: List[EnvironmentMode] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def load_global_config() -> GlobalConfig:
|
|
33
|
+
if not GLOBAL_CONFIG_FILE.exists():
|
|
34
|
+
return GlobalConfig()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
38
|
+
data = yaml.safe_load(f) or {}
|
|
39
|
+
return GlobalConfig(**data)
|
|
40
|
+
except Exception:
|
|
41
|
+
return GlobalConfig()
|
|
42
|
+
|
|
43
|
+
def save_global_config(config: GlobalConfig):
|
|
44
|
+
flowstash_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
with open(GLOBAL_CONFIG_FILE, "w") as f:
|
|
46
|
+
yaml.safe_dump(config.model_dump(), f)
|
|
47
|
+
|
|
48
|
+
def get_access_token() -> Optional[str]:
|
|
49
|
+
return keyring.get_password(SERVICE_NAME, TOKEN_KEY)
|
|
50
|
+
|
|
51
|
+
def set_access_token(token: str):
|
|
52
|
+
keyring.set_password(SERVICE_NAME, TOKEN_KEY, token)
|
|
53
|
+
|
|
54
|
+
def delete_access_token():
|
|
55
|
+
try:
|
|
56
|
+
keyring.delete_password(SERVICE_NAME, TOKEN_KEY)
|
|
57
|
+
except keyring.errors.PasswordDeleteError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def load_project_config() -> Optional[ProjectConfig]:
|
|
61
|
+
path = Path(PROJECT_CONFIG_FILE)
|
|
62
|
+
if not path.exists():
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(path, "r") as f:
|
|
67
|
+
data = yaml.safe_load(f) or {}
|
|
68
|
+
return ProjectConfig(**data)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def save_project_config(config: ProjectConfig):
|
|
73
|
+
with open(PROJECT_CONFIG_FILE, "w") as f:
|
|
74
|
+
yaml.safe_dump(config.model_dump(), f)
|
|
75
|
+
|
|
76
|
+
# Legacy aliases for compatibility if needed, though we should update callers
|
|
77
|
+
def load_config() -> GlobalConfig:
|
|
78
|
+
return load_global_config()
|
|
79
|
+
|
|
80
|
+
def save_config(config: GlobalConfig):
|
|
81
|
+
save_global_config(config)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
|
|
5
|
+
def check_docker_binary() -> bool:
|
|
6
|
+
return shutil.which("docker") is not None
|
|
7
|
+
|
|
8
|
+
def check_docker_compose_binary() -> bool:
|
|
9
|
+
# Check for 'docker compose' (V2) or 'docker-compose' (V1)
|
|
10
|
+
if shutil.which("docker-compose"):
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
# Check if 'docker compose' works
|
|
14
|
+
try:
|
|
15
|
+
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
|
16
|
+
return True
|
|
17
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def check_docker_daemon() -> Tuple[bool, Optional[str]]:
|
|
21
|
+
"""Returns (is_running, error_message)"""
|
|
22
|
+
try:
|
|
23
|
+
subprocess.run(["docker", "info"], capture_output=True, check=True)
|
|
24
|
+
return True, None
|
|
25
|
+
except subprocess.CalledProcessError as e:
|
|
26
|
+
return False, e.stderr.decode().strip()
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
return False, "Docker binary not found"
|
|
29
|
+
|
|
30
|
+
def get_docker_compose_cmd() -> list:
|
|
31
|
+
if shutil.which("docker-compose"):
|
|
32
|
+
return ["docker-compose"]
|
|
33
|
+
return ["docker", "compose"]
|
flowstash/main.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import click
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .commands import auth as auth_cmds
|
|
5
|
+
from .commands import project as project_cmds
|
|
6
|
+
from .commands import build as build_cmds
|
|
7
|
+
from .commands import deploy as deploy_cmds
|
|
8
|
+
from .commands import run as run_cmds
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = importlib.metadata.version("flowstash-cli")
|
|
14
|
+
except importlib.metadata.PackageNotFoundError:
|
|
15
|
+
__version__ = "unknown"
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="flowstash",
|
|
21
|
+
help=f"""
|
|
22
|
+
# 🚀 flowstash CLI (v{__version__})
|
|
23
|
+
Manage your flowstash projects and deployments with ease.
|
|
24
|
+
|
|
25
|
+
## 🛠 Usage
|
|
26
|
+
Run commands below to initialize, build, and deploy your integrations.
|
|
27
|
+
""",
|
|
28
|
+
add_completion=False,
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
rich_markup_mode="rich"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
env_app = typer.Typer(
|
|
34
|
+
name="env",
|
|
35
|
+
help="Manage environments",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
rich_markup_mode="rich"
|
|
38
|
+
)
|
|
39
|
+
app.add_typer(env_app, name="env")
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def help(ctx: typer.Context):
|
|
43
|
+
"""
|
|
44
|
+
Show help information for all commands.
|
|
45
|
+
"""
|
|
46
|
+
console.print(ctx.parent.get_help()) if ctx.parent else console.print(ctx.get_help())
|
|
47
|
+
|
|
48
|
+
# Shortcuts / Top-level commands
|
|
49
|
+
@app.command()
|
|
50
|
+
def login(
|
|
51
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="Username for manual login"),
|
|
52
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Password for manual login")
|
|
53
|
+
):
|
|
54
|
+
"""Log in to the flowstash Managed Platform."""
|
|
55
|
+
auth_cmds.login(username=username, password=password)
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def init(
|
|
59
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Project name"),
|
|
60
|
+
fix: bool = typer.Option(False, "--fix", help="Repair missing components"),
|
|
61
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
[bold green]Initialize[/bold green] or [bold yellow]repair[/bold yellow] a flowstash project.
|
|
65
|
+
|
|
66
|
+
This command sets up the local structure, creates the `.flowstash` config,
|
|
67
|
+
and lets you configure your first environments.
|
|
68
|
+
"""
|
|
69
|
+
project_cmds.init(name=name, fix=fix, force=force)
|
|
70
|
+
|
|
71
|
+
@app.command()
|
|
72
|
+
def check():
|
|
73
|
+
"""Inspect the current project structure."""
|
|
74
|
+
project_cmds.check()
|
|
75
|
+
|
|
76
|
+
@app.command()
|
|
77
|
+
def link():
|
|
78
|
+
"""
|
|
79
|
+
[bold cyan]Link[/bold cyan] your current project to a flowstash Managed Project.
|
|
80
|
+
"""
|
|
81
|
+
from .core.config import load_project_config
|
|
82
|
+
from .commands.project import _link_project
|
|
83
|
+
|
|
84
|
+
project_config = load_project_config()
|
|
85
|
+
if not project_config:
|
|
86
|
+
console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
|
|
87
|
+
raise typer.Exit(code=1)
|
|
88
|
+
|
|
89
|
+
_link_project(project_config)
|
|
90
|
+
|
|
91
|
+
@env_app.command("add")
|
|
92
|
+
def env_add(
|
|
93
|
+
env_name: Optional[str] = typer.Argument(None, help="Environment name to add"),
|
|
94
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
[bold blue]Add a new environment[/bold blue] to your project.
|
|
98
|
+
|
|
99
|
+
Choose between Local, Dev, Prod, or Custom. It will automatically filter
|
|
100
|
+
out existing environments and scaffold necessary files.
|
|
101
|
+
"""
|
|
102
|
+
project_cmds.env_add(env_name=env_name, force=force)
|
|
103
|
+
|
|
104
|
+
@env_app.command("del")
|
|
105
|
+
def env_del(
|
|
106
|
+
env_name: str = typer.Argument(..., help="Environment name to delete")
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
[bold red]Delete an environment[/bold red] from your project.
|
|
110
|
+
|
|
111
|
+
Requires confirmation before removal.
|
|
112
|
+
"""
|
|
113
|
+
project_cmds.env_delete(env_name=env_name)
|
|
114
|
+
|
|
115
|
+
@env_app.command("init-vscode")
|
|
116
|
+
def env_init_vscode(
|
|
117
|
+
env_name: str = typer.Argument(..., help="Environment name to initialize VS Code for"),
|
|
118
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing launch.json if it's malformed or already has the configuration")
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
[bold blue]Initialize VS Code launch configurations[/bold blue] for an environment.
|
|
122
|
+
|
|
123
|
+
This command will add 'Launch <env> API' and 'Launch <env> Worker' to your .vscode/launch.json,
|
|
124
|
+
allowing you to easily debug your environment's API and Worker using VS Code.
|
|
125
|
+
"""
|
|
126
|
+
project_cmds.init_vscode_launch(env_name=env_name, force=force)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def run(
|
|
131
|
+
ctx: typer.Context,
|
|
132
|
+
env: str = typer.Argument("local", help="Environment to run (default: local)"),
|
|
133
|
+
build: bool = typer.Option(True, "--build/--no-build", help="Build images before starting"),
|
|
134
|
+
detach: bool = typer.Option(False, "--detach", "-d", help="Run in background"),
|
|
135
|
+
logs: bool = typer.Option(False, "--logs", help="Follow logs (useful with --detach)")
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
[bold magenta]Run[/bold magenta] the project locally using Docker Compose.
|
|
139
|
+
|
|
140
|
+
Defaults to the 'local' environment.
|
|
141
|
+
|
|
142
|
+
[yellow]Note:[/yellow] To run a specific environment, use: [bold]flowstash run <env_name>[/bold]
|
|
143
|
+
"""
|
|
144
|
+
from .core.config import load_project_config
|
|
145
|
+
from rich.prompt import Confirm
|
|
146
|
+
|
|
147
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
148
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
149
|
+
|
|
150
|
+
project_config = load_project_config()
|
|
151
|
+
if project_config:
|
|
152
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
153
|
+
if not env_mode:
|
|
154
|
+
if env == "local":
|
|
155
|
+
if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
156
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
157
|
+
else:
|
|
158
|
+
console.print(f"[red]Aborting. Run 'flowstash env add' to create it manually.[/red]")
|
|
159
|
+
raise typer.Exit(code=1)
|
|
160
|
+
else:
|
|
161
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
162
|
+
raise typer.Exit(code=1)
|
|
163
|
+
|
|
164
|
+
run_cmds.run(env=env, build=build, detach=detach, logs=logs)
|
|
165
|
+
|
|
166
|
+
@app.command()
|
|
167
|
+
def build(
|
|
168
|
+
ctx: typer.Context,
|
|
169
|
+
env: str = typer.Argument("local", help="Environment to build (default: local)"),
|
|
170
|
+
tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
|
|
171
|
+
):
|
|
172
|
+
"""
|
|
173
|
+
[bold yellow]Build[/bold yellow] project artifacts/images.
|
|
174
|
+
|
|
175
|
+
Defaults to the 'local' environment.
|
|
176
|
+
|
|
177
|
+
[yellow]Note:[/yellow] To build for a specific environment, use: [bold]flowstash build <env_name>[/bold]
|
|
178
|
+
"""
|
|
179
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
180
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
181
|
+
|
|
182
|
+
from .core.config import load_project_config
|
|
183
|
+
from rich.prompt import Confirm
|
|
184
|
+
|
|
185
|
+
project_config = load_project_config()
|
|
186
|
+
if project_config:
|
|
187
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
188
|
+
if not env_mode:
|
|
189
|
+
if env == "local":
|
|
190
|
+
if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
191
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
192
|
+
else:
|
|
193
|
+
console.print(f"[red]Aborting. Run 'flowstash env add' to create it manually.[/red]")
|
|
194
|
+
raise typer.Exit(code=1)
|
|
195
|
+
else:
|
|
196
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
|
|
199
|
+
build_cmds.build(env=env, tag=tag)
|
|
200
|
+
|
|
201
|
+
@app.command()
|
|
202
|
+
def deploy(
|
|
203
|
+
ctx: typer.Context,
|
|
204
|
+
env: str = typer.Argument("prod", help="Environment to deploy to (default: prod)"),
|
|
205
|
+
artifact: Optional[str] = typer.Option(None, "--artifact", "-a", help="Artifact ID to deploy"),
|
|
206
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Do not ask for confirmation")
|
|
207
|
+
):
|
|
208
|
+
"""
|
|
209
|
+
[bold cyan]Deploy[/bold cyan] your project to the flowstash Managed Platform.
|
|
210
|
+
|
|
211
|
+
Defaults to the 'prod' environment. If 'prod' is missing, it will prompt you to set it up.
|
|
212
|
+
|
|
213
|
+
[yellow]Note:[/yellow] To deploy to a specific environment, use: [bold]flowstash deploy <env_name>[/bold]
|
|
214
|
+
"""
|
|
215
|
+
from .core.config import load_project_config
|
|
216
|
+
from .commands.project import add_environment
|
|
217
|
+
from rich.prompt import Confirm
|
|
218
|
+
|
|
219
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
220
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
221
|
+
|
|
222
|
+
project_config = load_project_config()
|
|
223
|
+
if not project_config:
|
|
224
|
+
console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
|
|
225
|
+
raise typer.Exit(code=1)
|
|
226
|
+
|
|
227
|
+
# Find the requested environment
|
|
228
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
229
|
+
|
|
230
|
+
if not env_mode:
|
|
231
|
+
if env == "prod":
|
|
232
|
+
if not non_interactive and Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
233
|
+
# We need to pass the actual project_config object to add_environment
|
|
234
|
+
# But project_cmds is a module, we should be careful about cyclic imports or just use it.
|
|
235
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
236
|
+
# Reload or refresh check
|
|
237
|
+
project_config = load_project_config()
|
|
238
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
239
|
+
if not env_mode:
|
|
240
|
+
console.print(f"[red]Environment '{env}' was not created. Aborting.[/red]")
|
|
241
|
+
raise typer.Exit(code=1)
|
|
242
|
+
else:
|
|
243
|
+
console.print(f"[red]Aborting. Use 'flowstash env add' to create environments manually.[/red]")
|
|
244
|
+
raise typer.Exit(code=1)
|
|
245
|
+
else:
|
|
246
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
247
|
+
console.print(f"[yellow]Available environments: {', '.join(e.name for e in project_config.environments)}[/yellow]")
|
|
248
|
+
console.print(f"To deploy to a specific environment, use: [bold]flowstash deploy <env_name>[/bold]")
|
|
249
|
+
raise typer.Exit(code=1)
|
|
250
|
+
|
|
251
|
+
deploy_cmds.deploy(env=env, artifact=artifact, non_interactive=non_interactive)
|
|
252
|
+
|
|
253
|
+
@app.command()
|
|
254
|
+
def whoami():
|
|
255
|
+
"""Show current login status."""
|
|
256
|
+
auth_cmds.whoami()
|
|
257
|
+
|
|
258
|
+
@app.command()
|
|
259
|
+
def logout():
|
|
260
|
+
"""Log out from the platform."""
|
|
261
|
+
auth_cmds.logout()
|
|
262
|
+
|
|
263
|
+
@app.command()
|
|
264
|
+
def version():
|
|
265
|
+
"""Show the flowstash CLI version."""
|
|
266
|
+
console.print(f"flowstash CLI version: [bold green]{__version__}[/bold green]")
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
app()
|