sscli 2.0.2__tar.gz → 2.1.0__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.
- {sscli-2.0.2 → sscli-2.1.0}/PKG-INFO +1 -1
- sscli-2.1.0/foundry/actions/development.py +397 -0
- sscli-2.1.0/foundry/actions/explore.py +139 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/cli.py +53 -8
- {sscli-2.0.2 → sscli-2.1.0}/foundry/interactive.py +9 -7
- {sscli-2.0.2 → sscli-2.1.0}/pyproject.toml +7 -20
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/PKG-INFO +1 -1
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/SOURCES.txt +1 -0
- sscli-2.0.2/foundry/actions/explore.py +0 -145
- {sscli-2.0.2 → sscli-2.1.0}/README.md +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/account_info.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/documentation.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/features.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/generation_utils.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/generator.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/health.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/interactive_config.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/metadata.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/repo_utils.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/setup.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/validation.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/models.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/reports.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/verifier.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verify.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/admin_tools.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/access_manager.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/manager.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/models.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/view.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha_expiration.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animated_experience.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/animation.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/assets.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/base.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/canvas.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/components.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/composites.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/core.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/animation_engine.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/clock.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/frame_buffer.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/state.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/rendering.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/entrance.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/forge.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/info_slide.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_disperse.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_dissolve.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_reveal.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_slide.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/rain.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/tranquility.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/sections/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/ui/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/utils.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/auth.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/client_access_manager.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/auth.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/interactive.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/constants.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/development_experience.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/gate.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/interactive_utils.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/internal_experience.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/manage.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/cli.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/licenses.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/manager.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/menus.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/reporter.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/runner.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/patch.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/telemetry.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/client/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/development/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/development/build_tools.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/internal/__init__.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/foundry/utils.py +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/setup.cfg +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/dependency_links.txt +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/entry_points.txt +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/requires.txt +0 -0
- {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Development tools for sscli - status, build, and swap functionality
|
|
3
|
+
Internal development only - not available in production installs
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import questionary
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from foundry.constants import console
|
|
10
|
+
from foundry.utils import is_internal_dev
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
import importlib.util
|
|
16
|
+
# Detect venv location
|
|
17
|
+
def get_venv_path():
|
|
18
|
+
"""Detect Python virtual environment."""
|
|
19
|
+
# First priority: VIRTUAL_ENV environment variable (most reliable)
|
|
20
|
+
if os.environ.get('VIRTUAL_ENV'):
|
|
21
|
+
venv = Path(os.environ['VIRTUAL_ENV'])
|
|
22
|
+
if (venv / 'bin' / 'pip').exists() or (venv / 'bin' / 'python').exists():
|
|
23
|
+
return venv
|
|
24
|
+
|
|
25
|
+
# Second priority: Try to find venv near the script
|
|
26
|
+
stack_cli_root = get_stack_cli_root()
|
|
27
|
+
for venv_candidate in ['venv', '.venv', 'env']:
|
|
28
|
+
venv_path = stack_cli_root / venv_candidate
|
|
29
|
+
if (venv_path / 'bin' / 'python').exists():
|
|
30
|
+
return venv_path
|
|
31
|
+
|
|
32
|
+
# Third priority: try using sys.prefix (venv's location when activated)
|
|
33
|
+
if hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix:
|
|
34
|
+
# We're in a virtual environment
|
|
35
|
+
return Path(sys.prefix)
|
|
36
|
+
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def get_stack_cli_root():
|
|
40
|
+
"""Get the stack-cli root directory."""
|
|
41
|
+
# First, try FOUNDRY_ROOT environment variable
|
|
42
|
+
if os.environ.get('FOUNDRY_ROOT'):
|
|
43
|
+
foundry_root = Path(os.environ['FOUNDRY_ROOT'])
|
|
44
|
+
stack_cli_path = foundry_root / 'tooling' / 'stack-cli'
|
|
45
|
+
if stack_cli_path.exists():
|
|
46
|
+
return stack_cli_path
|
|
47
|
+
|
|
48
|
+
# Try to find by looking for pyproject.toml with sscli package
|
|
49
|
+
current_file = Path(__file__).resolve()
|
|
50
|
+
|
|
51
|
+
# Go up from foundry/actions/development.py -> stack-cli root
|
|
52
|
+
candidate = current_file.parent.parent.parent
|
|
53
|
+
if (candidate / 'pyproject.toml').exists():
|
|
54
|
+
with open(candidate / 'pyproject.toml') as f:
|
|
55
|
+
content = f.read()
|
|
56
|
+
if 'name = "sscli"' in content:
|
|
57
|
+
return candidate
|
|
58
|
+
|
|
59
|
+
# Fallback: use sys.prefix for installed package case
|
|
60
|
+
return Path(sys.prefix)
|
|
61
|
+
|
|
62
|
+
def get_local_version():
|
|
63
|
+
"""Get version from local pyproject.toml."""
|
|
64
|
+
stack_cli_root = get_stack_cli_root()
|
|
65
|
+
pyproject = stack_cli_root / 'pyproject.toml'
|
|
66
|
+
|
|
67
|
+
if pyproject.exists():
|
|
68
|
+
with open(pyproject) as f:
|
|
69
|
+
for line in f:
|
|
70
|
+
if line.startswith('version'):
|
|
71
|
+
return line.split('=')[1].strip().strip('"')
|
|
72
|
+
return "unknown"
|
|
73
|
+
|
|
74
|
+
def get_pip_version():
|
|
75
|
+
"""Get installed pip package version."""
|
|
76
|
+
try:
|
|
77
|
+
return get_package_version('sscli')
|
|
78
|
+
except:
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
def is_using_local():
|
|
82
|
+
"""Check if currently using local build."""
|
|
83
|
+
stack_cli_root = get_stack_cli_root()
|
|
84
|
+
return str(stack_cli_root) in sys.executable
|
|
85
|
+
|
|
86
|
+
def run_build(clean=False):
|
|
87
|
+
"""Build distribution packages using setuptools."""
|
|
88
|
+
stack_cli_root = get_stack_cli_root()
|
|
89
|
+
|
|
90
|
+
console.print("[bold cyan]→[/bold cyan] [bold]Building packages...[/bold]")
|
|
91
|
+
console.print("")
|
|
92
|
+
|
|
93
|
+
# Clean if requested
|
|
94
|
+
if clean:
|
|
95
|
+
dist_dir = stack_cli_root / 'dist'
|
|
96
|
+
build_dir = stack_cli_root / 'build'
|
|
97
|
+
|
|
98
|
+
if dist_dir.exists():
|
|
99
|
+
console.print("[yellow]Removing dist/[/yellow]")
|
|
100
|
+
import shutil
|
|
101
|
+
shutil.rmtree(dist_dir)
|
|
102
|
+
|
|
103
|
+
if build_dir.exists():
|
|
104
|
+
console.print("[yellow]Removing build/[/yellow]")
|
|
105
|
+
import shutil
|
|
106
|
+
shutil.rmtree(build_dir)
|
|
107
|
+
|
|
108
|
+
# Create dist directory
|
|
109
|
+
dist_dir = stack_cli_root / 'dist'
|
|
110
|
+
dist_dir.mkdir(exist_ok=True)
|
|
111
|
+
|
|
112
|
+
# Run wheel build
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[sys.executable, "-m", "pip", "wheel", ".", "--no-deps", "-w", str(dist_dir)],
|
|
116
|
+
cwd=str(stack_cli_root),
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if result.returncode != 0:
|
|
122
|
+
console.print(f"[red]Build failed:[/red]")
|
|
123
|
+
console.print(result.stderr)
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
# Build sdist using tarball
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
[sys.executable, "-m", "pip", "download", ".", "--no-deps", "--no-binary", ":all:", "-d", str(dist_dir)],
|
|
129
|
+
cwd=str(stack_cli_root),
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Show build artifacts
|
|
135
|
+
if dist_dir.exists():
|
|
136
|
+
console.print("")
|
|
137
|
+
artifacts = sorted(list(dist_dir.glob('*')))
|
|
138
|
+
if artifacts:
|
|
139
|
+
console.print("[bold]✓[/bold] Build complete")
|
|
140
|
+
console.print("[bold cyan]→[/bold cyan] Build artifacts:")
|
|
141
|
+
console.print("")
|
|
142
|
+
for artifact in artifacts:
|
|
143
|
+
size_kb = artifact.stat().st_size / 1024
|
|
144
|
+
console.print(f" [cyan]{artifact.name}[/cyan] ({size_kb:.0f}K)")
|
|
145
|
+
console.print("")
|
|
146
|
+
console.print("[bold cyan]Next steps:[/bold cyan]")
|
|
147
|
+
console.print(" • Install locally: [yellow]pip install --force-reinstall ./dist/sscli-*.whl[/yellow]")
|
|
148
|
+
console.print(" • Or use swap: [yellow]sscli swap[/yellow]")
|
|
149
|
+
return True
|
|
150
|
+
except Exception as e:
|
|
151
|
+
console.print(f"[red]✗ Build failed: {e}[/red]")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def show_status(verbose=False):
|
|
157
|
+
"""Show development environment status."""
|
|
158
|
+
stack_cli_root = get_stack_cli_root()
|
|
159
|
+
venv_path = get_venv_path()
|
|
160
|
+
local_version = get_local_version()
|
|
161
|
+
pip_version = get_pip_version()
|
|
162
|
+
using_local = is_using_local()
|
|
163
|
+
|
|
164
|
+
# Header
|
|
165
|
+
console.print("")
|
|
166
|
+
header_panel = Panel(
|
|
167
|
+
"[bold]sscli Development Status[/bold]",
|
|
168
|
+
border_style="blue",
|
|
169
|
+
padding=(0, 2)
|
|
170
|
+
)
|
|
171
|
+
console.print(header_panel)
|
|
172
|
+
console.print("")
|
|
173
|
+
|
|
174
|
+
# Create version table
|
|
175
|
+
version_table = Table(show_header=False, show_lines=False, padding=(0, 2))
|
|
176
|
+
version_table.add_column()
|
|
177
|
+
version_table.add_column()
|
|
178
|
+
|
|
179
|
+
version_table.add_row(" [bold]Local build:[/bold]", f"[yellow]v{local_version}[/yellow] in [dim]{stack_cli_root}[/dim]")
|
|
180
|
+
version_table.add_row(" [bold]Pip package:[/bold]", f"[yellow]{pip_version if pip_version else '(not installed)'}[/yellow]")
|
|
181
|
+
|
|
182
|
+
console.print(version_table)
|
|
183
|
+
console.print("")
|
|
184
|
+
|
|
185
|
+
# Installation status
|
|
186
|
+
current_status = "[yellow]LOCAL BUILD (development)[/yellow]" if using_local else f"[yellow]PIP PACKAGE (v{pip_version})[/yellow]" if pip_version else "[yellow]UNKNOWN[/yellow]"
|
|
187
|
+
|
|
188
|
+
status_table = Table(show_header=False, show_lines=False, padding=(0, 2))
|
|
189
|
+
status_table.add_column()
|
|
190
|
+
status_table.add_column()
|
|
191
|
+
status_table.add_row(" [bold]Currently using:[/bold]", current_status)
|
|
192
|
+
|
|
193
|
+
console.print(status_table)
|
|
194
|
+
console.print("")
|
|
195
|
+
|
|
196
|
+
# Environment info
|
|
197
|
+
env_table = Table(show_header=False, show_lines=False, padding=(0, 2))
|
|
198
|
+
env_table.add_column()
|
|
199
|
+
env_table.add_column()
|
|
200
|
+
env_table.add_row(" [bold]Python:[/bold]", f"{sys.version.split()[0]}")
|
|
201
|
+
env_table.add_row(" [bold]Venv:[/bold]", str(venv_path) if venv_path else "[yellow](not detected)[/yellow]")
|
|
202
|
+
|
|
203
|
+
if verbose and venv_path:
|
|
204
|
+
req_file = stack_cli_root / 'requirements.txt'
|
|
205
|
+
if req_file.exists():
|
|
206
|
+
with open(req_file) as f:
|
|
207
|
+
deps = [line.strip() for line in f if line.strip() and not line.startswith('#')]
|
|
208
|
+
env_table.add_row(" [bold]Dependencies:[/bold]", f"{len(deps)} packages")
|
|
209
|
+
|
|
210
|
+
console.print(env_table)
|
|
211
|
+
console.print("")
|
|
212
|
+
|
|
213
|
+
# Quick actions
|
|
214
|
+
console.print(" [bold cyan]Quick Actions:[/bold cyan]")
|
|
215
|
+
console.print(" • Show status verbose: [yellow]sscli status --verbose[/yellow]")
|
|
216
|
+
console.print(" • Build packages: [yellow]sscli build[/yellow]")
|
|
217
|
+
console.print(" • Switch installation: [yellow]sscli swap[/yellow]")
|
|
218
|
+
console.print("")
|
|
219
|
+
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
def run_swap(target=None):
|
|
223
|
+
"""Swap between local and pip versions."""
|
|
224
|
+
stack_cli_root = get_stack_cli_root()
|
|
225
|
+
venv_path = get_venv_path()
|
|
226
|
+
pip_version = get_pip_version()
|
|
227
|
+
using_local = is_using_local()
|
|
228
|
+
|
|
229
|
+
if not venv_path:
|
|
230
|
+
console.print("[red]✗ Virtual environment not found[/red]")
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
venv_bin = venv_path / 'bin'
|
|
234
|
+
|
|
235
|
+
# If no target specified, ask interactively
|
|
236
|
+
if target is None:
|
|
237
|
+
console.print("")
|
|
238
|
+
console.print("[bold]Package Swap Utility[/bold]")
|
|
239
|
+
console.print("")
|
|
240
|
+
|
|
241
|
+
status = "[yellow]LOCAL BUILD[/yellow]" if using_local else f"[yellow]PIP PACKAGE (v{pip_version})[/yellow]" if pip_version else "[yellow]UNKNOWN[/yellow]"
|
|
242
|
+
console.print(f" Currently: {status}")
|
|
243
|
+
console.print("")
|
|
244
|
+
|
|
245
|
+
available = []
|
|
246
|
+
if not using_local:
|
|
247
|
+
available.append(("1", "Local build", "local", "v" + get_local_version()))
|
|
248
|
+
if pip_version:
|
|
249
|
+
available.append(("2", "Pip package", "pip", "v" + pip_version))
|
|
250
|
+
if not available:
|
|
251
|
+
available.append(("1", "Local build", "local", "v" + get_local_version()))
|
|
252
|
+
|
|
253
|
+
console.print(" [bold]Available versions:[/bold]")
|
|
254
|
+
for num, label, val, ver in available:
|
|
255
|
+
console.print(f" {num}) {label}: {ver}")
|
|
256
|
+
console.print("")
|
|
257
|
+
|
|
258
|
+
choice = questionary.select(
|
|
259
|
+
"Switch to which installation?",
|
|
260
|
+
choices=[f"{label} ({ver})" for _, label, _, ver in available] + ["Cancel"]
|
|
261
|
+
).ask()
|
|
262
|
+
|
|
263
|
+
if choice and choice != "Cancel":
|
|
264
|
+
target = available[[f"{label} ({ver})" for _, label, _, ver in available].index(choice)][2]
|
|
265
|
+
else:
|
|
266
|
+
return True # User cancelled
|
|
267
|
+
|
|
268
|
+
console.print("")
|
|
269
|
+
console.print(f"[bold cyan]→[/bold cyan] [bold]Switching to {target}...[/bold]")
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
if target == "local":
|
|
273
|
+
# Install from local dist
|
|
274
|
+
dist_dir = stack_cli_root / 'dist'
|
|
275
|
+
wheels = list(dist_dir.glob('sscli-*.whl'))
|
|
276
|
+
|
|
277
|
+
if not wheels:
|
|
278
|
+
console.print("[red]✗ No wheel found in dist/. Run 'sscli build' first[/red]")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
wheel = wheels[0]
|
|
282
|
+
console.print(f" Installing from [cyan]{wheel.name}[/cyan]...")
|
|
283
|
+
|
|
284
|
+
result = subprocess.run(
|
|
285
|
+
[str(venv_bin / 'pip'), 'install', '--force-reinstall', str(wheel)],
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if result.returncode != 0:
|
|
291
|
+
console.print("[red]✗ Installation failed[/red]")
|
|
292
|
+
if "--force-reinstall" in result.stderr:
|
|
293
|
+
console.print(result.stderr)
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
console.print("[green]✓ Switched to local build[/green]")
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
elif target == "pip":
|
|
300
|
+
if not pip_version:
|
|
301
|
+
console.print("[red]✗ sscli not available on PyPI. Build and install locally[/red]")
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
console.print(f" Installing sscli=={pip_version} from PyPI...")
|
|
305
|
+
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
[str(venv_bin / 'pip'), 'install', '--force-reinstall', f'sscli=={pip_version}'],
|
|
308
|
+
capture_output=True,
|
|
309
|
+
text=True
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if result.returncode != 0:
|
|
313
|
+
console.print("[red]✗ Installation failed[/red]")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
console.print("[green]✓ Switched to pip package[/green]")
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
console.print(f"[red]✗ Swap failed: {e}[/red]")
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
def dev_tools_menu():
|
|
326
|
+
"""Interactive menu for development tools."""
|
|
327
|
+
# Check if running in development context
|
|
328
|
+
if not is_internal_dev():
|
|
329
|
+
console.print("[yellow]⚠️ Development tools are only available in development mode.[/yellow]")
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
console.clear()
|
|
333
|
+
|
|
334
|
+
console.print("[bold cyan]Development Tools[/bold cyan]")
|
|
335
|
+
console.print("[dim]Utilities for local development and testing[/dim]")
|
|
336
|
+
console.print("")
|
|
337
|
+
|
|
338
|
+
while True:
|
|
339
|
+
choices = [
|
|
340
|
+
questionary.Choice("📊 Show Development Status", value="status"),
|
|
341
|
+
questionary.Choice("🔨 Build Distribution Package", value="build"),
|
|
342
|
+
questionary.Choice("🔄 Switch Build Source (Local ↔ Pip)", value="swap"),
|
|
343
|
+
questionary.Choice("← Back to Main Menu", value="back"),
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
selection = questionary.select(
|
|
347
|
+
"Select a development tool:",
|
|
348
|
+
choices=choices,
|
|
349
|
+
).ask()
|
|
350
|
+
|
|
351
|
+
if selection is None or selection == "back":
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
console.clear()
|
|
355
|
+
console.print("")
|
|
356
|
+
|
|
357
|
+
if selection == "status":
|
|
358
|
+
show_verbose = questionary.confirm(
|
|
359
|
+
"Show detailed dependency information?",
|
|
360
|
+
default=False
|
|
361
|
+
).ask()
|
|
362
|
+
show_status(verbose=show_verbose)
|
|
363
|
+
|
|
364
|
+
elif selection == "build":
|
|
365
|
+
clean = questionary.confirm(
|
|
366
|
+
"Clean build artifacts before building?",
|
|
367
|
+
default=False
|
|
368
|
+
).ask()
|
|
369
|
+
if run_build(clean=clean):
|
|
370
|
+
console.print("[green]✓[/green] Build successful")
|
|
371
|
+
else:
|
|
372
|
+
console.print("[red]✗[/red] Build failed")
|
|
373
|
+
|
|
374
|
+
elif selection == "swap":
|
|
375
|
+
# Show current status first
|
|
376
|
+
show_status(verbose=False)
|
|
377
|
+
console.print("")
|
|
378
|
+
|
|
379
|
+
swap_choice = questionary.select(
|
|
380
|
+
"Choose action:",
|
|
381
|
+
choices=[
|
|
382
|
+
questionary.Choice("🔄 Toggle (interactive)", value="toggle"),
|
|
383
|
+
questionary.Choice("📦 Switch to Local Build", value="local"),
|
|
384
|
+
questionary.Choice("🐍 Switch to Pip Package", value="pip"),
|
|
385
|
+
questionary.Choice("❌ Cancel", value="cancel"),
|
|
386
|
+
],
|
|
387
|
+
).ask()
|
|
388
|
+
|
|
389
|
+
if swap_choice and swap_choice != "cancel":
|
|
390
|
+
if run_swap(target=None if swap_choice == "toggle" else swap_choice):
|
|
391
|
+
console.print("[green]✓[/green] Swap successful")
|
|
392
|
+
else:
|
|
393
|
+
console.print("[red]✗[/red] Swap failed")
|
|
394
|
+
|
|
395
|
+
console.print("")
|
|
396
|
+
input("[dim]Press Enter to continue...[/dim]")
|
|
397
|
+
console.clear()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from foundry.constants import console, TIER_HIERARCHY, QUESTIONARY_STYLE, TEMPLATE_REGISTRY
|
|
2
|
+
from foundry.utils import get_templates, get_template_info
|
|
3
|
+
from foundry.auth import get_current_license_info
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.tree import Tree
|
|
6
|
+
import questionary
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_template_tech(name):
|
|
11
|
+
"""Get technology stack for a template by name."""
|
|
12
|
+
# Map template names to tech descriptions
|
|
13
|
+
tech_map = {
|
|
14
|
+
"python-saas": "Python (FastAPI/SQLAlchemy)",
|
|
15
|
+
"rails-api": "Ruby on Rails",
|
|
16
|
+
"react-client": "React/Vite",
|
|
17
|
+
"rails-ui-kit": "Rails + ViewComponent",
|
|
18
|
+
"static-landing": "Astro (Static Site)",
|
|
19
|
+
"mobile-android": "Android (Kotlin/Compose)",
|
|
20
|
+
"mobile-ios": "iOS (Swift/SwiftUI)",
|
|
21
|
+
"data-pipeline": "Data Stack (dbt + Python)",
|
|
22
|
+
"terraform-infra": "Terraform (Multi-cloud)",
|
|
23
|
+
"wiring": "Docker Orchestration",
|
|
24
|
+
}
|
|
25
|
+
return tech_map.get(name, "Multi-tech Stack")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _strip_markup(text):
|
|
29
|
+
"""Remove Rich markup tags from text."""
|
|
30
|
+
return re.sub(r'\[/?[^\]]+\]', '', text)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _render_templates_tree(buckets):
|
|
34
|
+
"""Render a tree view with all categories expanded showing available templates."""
|
|
35
|
+
tree = Tree("[bold green]Seed & Source Inventory[/bold green]")
|
|
36
|
+
|
|
37
|
+
for category, items in buckets.items():
|
|
38
|
+
if not items:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
count = len(items)
|
|
42
|
+
cat_node = tree.add(f"[bold yellow]▼ {category} ({count})[/bold yellow]")
|
|
43
|
+
|
|
44
|
+
for item in items:
|
|
45
|
+
node = cat_node.add(f"[bold cyan]{item['name']}[/bold cyan]")
|
|
46
|
+
# Show tier badge in tree
|
|
47
|
+
if item['tier'] != "free":
|
|
48
|
+
tier_display = f"(🔐 {item['tier'].upper()})" if item['locked'] else f"(✅ {item['tier'].upper()})"
|
|
49
|
+
node.add(f"[yellow]{tier_display}[/yellow]")
|
|
50
|
+
# Show tech
|
|
51
|
+
tech = _get_template_tech(item['name'])
|
|
52
|
+
node.add(f"[dim]{tech}[/dim]")
|
|
53
|
+
|
|
54
|
+
console.print(Panel(tree, title="Available Templates", border_style="green"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_explore():
|
|
58
|
+
"""Interactive template explorer with expanded tree and selectable templates."""
|
|
59
|
+
# Get user info for tier checking
|
|
60
|
+
user_info = get_current_license_info()
|
|
61
|
+
user_tier = user_info.get("tier", "free")
|
|
62
|
+
user_rank = TIER_HIERARCHY.get(user_tier, 0)
|
|
63
|
+
|
|
64
|
+
# Group templates by category
|
|
65
|
+
buckets = {
|
|
66
|
+
"Backend Services": [],
|
|
67
|
+
"Frontend & UI": [],
|
|
68
|
+
"Mobile App": [],
|
|
69
|
+
"Data Engineering": [],
|
|
70
|
+
"Infrastructure": [],
|
|
71
|
+
"Other": [],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
templates = get_templates()
|
|
75
|
+
|
|
76
|
+
for tmpl in templates:
|
|
77
|
+
name = tmpl.name
|
|
78
|
+
# Fetch metadata to get the tier
|
|
79
|
+
info = get_template_info(name)
|
|
80
|
+
tmpl_tier = info.get("tier", "free").lower()
|
|
81
|
+
tmpl_rank = TIER_HIERARCHY.get(tmpl_tier, 0)
|
|
82
|
+
|
|
83
|
+
# Check if locked
|
|
84
|
+
locked = tmpl_rank > user_rank
|
|
85
|
+
|
|
86
|
+
# Simple menu label (no markup for questionary)
|
|
87
|
+
menu_label = name
|
|
88
|
+
if locked:
|
|
89
|
+
menu_label = f"{name} 🔐"
|
|
90
|
+
elif tmpl_tier != "free":
|
|
91
|
+
menu_label = f"{name} ✅"
|
|
92
|
+
|
|
93
|
+
# Store template data for processing
|
|
94
|
+
tmpl_data = {
|
|
95
|
+
"name": name,
|
|
96
|
+
"menu_label": menu_label,
|
|
97
|
+
"tier": tmpl_tier,
|
|
98
|
+
"locked": locked,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if name in ["python-saas", "rails-api"]:
|
|
102
|
+
buckets["Backend Services"].append(tmpl_data)
|
|
103
|
+
elif name in ["react-client", "rails-ui-kit", "static-landing"]:
|
|
104
|
+
buckets["Frontend & UI"].append(tmpl_data)
|
|
105
|
+
elif name.startswith("mobile-"):
|
|
106
|
+
buckets["Mobile App"].append(tmpl_data)
|
|
107
|
+
elif name == "data-pipeline":
|
|
108
|
+
buckets["Data Engineering"].append(tmpl_data)
|
|
109
|
+
elif name == "terraform-infra":
|
|
110
|
+
buckets["Infrastructure"].append(tmpl_data)
|
|
111
|
+
else:
|
|
112
|
+
buckets["Other"].append(tmpl_data)
|
|
113
|
+
|
|
114
|
+
# Remove empty categories
|
|
115
|
+
buckets = {k: v for k, v in buckets.items() if v}
|
|
116
|
+
|
|
117
|
+
# Display the tree with all categories expanded
|
|
118
|
+
console.clear()
|
|
119
|
+
_render_templates_tree(buckets)
|
|
120
|
+
|
|
121
|
+
# Build flat list of templates for selection (without Rich markup for questionary)
|
|
122
|
+
template_choices = []
|
|
123
|
+
for category, items in buckets.items():
|
|
124
|
+
for item in items:
|
|
125
|
+
template_choices.append((item['name'], item['menu_label']))
|
|
126
|
+
|
|
127
|
+
# Add back button
|
|
128
|
+
template_choices.append(("back", "← Back to Main Menu"))
|
|
129
|
+
|
|
130
|
+
# Show template selection menu
|
|
131
|
+
selected = questionary.select(
|
|
132
|
+
"Select a template to view details:",
|
|
133
|
+
choices=[questionary.Choice(label, value=name) for name, label in template_choices],
|
|
134
|
+
style=questionary.Style(QUESTIONARY_STYLE)
|
|
135
|
+
).ask()
|
|
136
|
+
|
|
137
|
+
if selected is None or selected == "back":
|
|
138
|
+
return
|
|
139
|
+
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from foundry.constants import console
|
|
3
3
|
import typer
|
|
4
|
-
from foundry.actions.documentation import run_view_docs
|
|
5
4
|
from foundry.actions.explore import run_explore
|
|
6
5
|
from foundry.actions.generator import run_generator
|
|
7
6
|
from foundry.actions.health import run_health_check
|
|
8
7
|
from foundry.actions.setup import run_setup
|
|
9
8
|
from foundry.actions.validation import run_smoke_tests
|
|
10
9
|
from foundry.actions.verify import run_verification
|
|
10
|
+
from foundry.actions.development import dev_tools_menu, show_status, run_build, run_swap
|
|
11
11
|
from foundry.auth import get_current_license_info, logout
|
|
12
12
|
from foundry.telemetry import log_event
|
|
13
13
|
from foundry.commands.auth import auth_app, run_whoami
|
|
@@ -16,7 +16,7 @@ from foundry.utils import is_internal_dev
|
|
|
16
16
|
|
|
17
17
|
app = typer.Typer(
|
|
18
18
|
help="Seed & Source CLI Tool",
|
|
19
|
-
no_args_is_help=
|
|
19
|
+
no_args_is_help=False,
|
|
20
20
|
add_completion=True,
|
|
21
21
|
)
|
|
22
22
|
|
|
@@ -30,6 +30,14 @@ if is_internal_dev():
|
|
|
30
30
|
app.add_typer(ops_app, name="ops", help="Internal operations and maintenance.")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
@app.callback(invoke_without_command=True)
|
|
34
|
+
def default_action(ctx: typer.Context):
|
|
35
|
+
"""Default action: show interactive menu if no command is provided."""
|
|
36
|
+
if ctx.invoked_subcommand is None:
|
|
37
|
+
from foundry.interactive import interactive_menu
|
|
38
|
+
interactive_menu()
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
@app.command("whoami")
|
|
34
42
|
def whoami():
|
|
35
43
|
run_whoami()
|
|
@@ -135,9 +143,46 @@ def verify(
|
|
|
135
143
|
log_event("COMMAND", "verify")
|
|
136
144
|
run_verification(include_docker=not skip_docker, output_reports=not no_reports)
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
# Only expose dev tools in internal development mode
|
|
147
|
+
if is_internal_dev():
|
|
148
|
+
@app.command("dev")
|
|
149
|
+
def dev():
|
|
150
|
+
"""Access development tools (status, build, swap)."""
|
|
151
|
+
log_event("COMMAND", "dev")
|
|
152
|
+
dev_tools_menu()
|
|
153
|
+
|
|
154
|
+
@app.command("status")
|
|
155
|
+
def status(
|
|
156
|
+
verbose: bool = typer.Option(
|
|
157
|
+
False, "--verbose", "-v", help="Show detailed dependency information"
|
|
158
|
+
),
|
|
159
|
+
):
|
|
160
|
+
"""Show development environment status (local build vs pip package)."""
|
|
161
|
+
log_event("COMMAND", "status")
|
|
162
|
+
show_status(verbose=verbose)
|
|
163
|
+
|
|
164
|
+
@app.command("build")
|
|
165
|
+
def build(
|
|
166
|
+
clean: bool = typer.Option(
|
|
167
|
+
False, "--clean", help="Clean build artifacts before building"
|
|
168
|
+
),
|
|
169
|
+
):
|
|
170
|
+
"""Build distribution packages (wheel and sdist)."""
|
|
171
|
+
log_event("COMMAND", "build")
|
|
172
|
+
if run_build(clean=clean):
|
|
173
|
+
console.print("[green]✓[/green] Build successful")
|
|
174
|
+
else:
|
|
175
|
+
console.print("[red]✗[/red] Build failed")
|
|
176
|
+
raise typer.Exit(code=1)
|
|
177
|
+
|
|
178
|
+
@app.command("swap")
|
|
179
|
+
def swap(
|
|
180
|
+
target: Optional[str] = typer.Option(
|
|
181
|
+
None, help="Target: 'local' or 'pip' (interactive if not specified)"
|
|
182
|
+
),
|
|
183
|
+
):
|
|
184
|
+
"""Switch between local and pip package installations."""
|
|
185
|
+
log_event("COMMAND", "swap")
|
|
186
|
+
if not run_swap(target=target):
|
|
187
|
+
console.print("[red]✗[/red] Swap failed")
|
|
188
|
+
raise typer.Exit(code=1)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import questionary
|
|
2
2
|
from questionary import Choice
|
|
3
|
-
from foundry.actions.documentation import run_view_docs
|
|
4
3
|
from foundry.actions.explore import run_explore
|
|
5
4
|
from foundry.actions.generator import run_generator
|
|
6
5
|
from foundry.actions.validation import run_smoke_tests
|
|
7
6
|
from foundry.actions.account_info import account_info_menu
|
|
7
|
+
from foundry.actions.development import dev_tools_menu
|
|
8
8
|
from foundry.alpha_expiration import get_expiration_manager
|
|
9
9
|
from foundry.alpha.view import show_warning_message
|
|
10
10
|
from foundry.auth import get_current_license_info, login_flow
|
|
@@ -95,9 +95,12 @@ def interactive_menu():
|
|
|
95
95
|
Choice("✨ Generate New Project", value="generate"),
|
|
96
96
|
Choice("⚙️ Manage Stack Configuration", value="config"),
|
|
97
97
|
Choice("🔍 Run System Validation (Smoke Tests)", value="validation"),
|
|
98
|
-
Choice("📚 View Documentation", value="docs"),
|
|
99
98
|
]
|
|
100
99
|
|
|
100
|
+
# Add development tools only in internal dev mode
|
|
101
|
+
if is_internal_dev():
|
|
102
|
+
base_choices.append(Choice("🛠️ Development Tools", value="dev_tools"))
|
|
103
|
+
|
|
101
104
|
# Add ADMIN-ONLY features if user is admin
|
|
102
105
|
if auth_info.get("tier") == "admin":
|
|
103
106
|
base_choices.append(Choice("🔑 Client Template Access Manager", value="client_access"))
|
|
@@ -125,8 +128,8 @@ def interactive_menu():
|
|
|
125
128
|
choice = "Manage Stack Configuration"
|
|
126
129
|
elif choice == "validation":
|
|
127
130
|
choice = "Run System Validation (Smoke Tests)"
|
|
128
|
-
elif choice == "
|
|
129
|
-
choice = "
|
|
131
|
+
elif choice == "dev_tools":
|
|
132
|
+
choice = "Development Tools"
|
|
130
133
|
elif choice == "client_access":
|
|
131
134
|
choice = "🔑 Client Template Access Manager"
|
|
132
135
|
elif choice == "login":
|
|
@@ -165,9 +168,8 @@ def interactive_menu():
|
|
|
165
168
|
elif choice == "Run System Validation (Smoke Tests)":
|
|
166
169
|
run_smoke_tests()
|
|
167
170
|
pause()
|
|
168
|
-
elif choice == "
|
|
169
|
-
|
|
170
|
-
pause()
|
|
171
|
+
elif choice == "Development Tools":
|
|
172
|
+
dev_tools_menu()
|
|
171
173
|
elif choice == "🔑 Client Template Access Manager":
|
|
172
174
|
from foundry.client_access_manager import client_access_admin_menu
|
|
173
175
|
client_access_admin_menu()
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [
|
|
3
|
-
"setuptools>=61.0",
|
|
4
|
-
]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
5
3
|
build-backend = "setuptools.build_meta"
|
|
6
4
|
|
|
7
5
|
[project]
|
|
8
6
|
name = "sscli"
|
|
9
|
-
version = "2.0
|
|
7
|
+
version = "2.1.0"
|
|
10
8
|
description = "Seed & Source CLI - Multi-tenant SaaS scaffolding with Seed & Source Core"
|
|
11
9
|
readme = "README.md"
|
|
12
10
|
requires-python = ">=3.9"
|
|
@@ -18,28 +16,17 @@ dependencies = [
|
|
|
18
16
|
"pathlib",
|
|
19
17
|
"python-dotenv",
|
|
20
18
|
"pydantic>=2.0.0",
|
|
21
|
-
"httpx>=0.27.0"
|
|
19
|
+
"httpx>=0.27.0"
|
|
22
20
|
]
|
|
23
21
|
|
|
24
22
|
[project.scripts]
|
|
25
23
|
sscli = "foundry.manage:app"
|
|
26
24
|
|
|
27
|
-
[tool.setuptools
|
|
28
|
-
include = [
|
|
29
|
-
"foundry*",
|
|
30
|
-
]
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
packages = { find = { include = ["foundry*"] } }
|
|
31
27
|
|
|
32
28
|
[tool.setuptools.package-data]
|
|
33
|
-
"*" = [
|
|
34
|
-
"*.md",
|
|
35
|
-
"*.json",
|
|
36
|
-
]
|
|
29
|
+
"*" = ["*.md", "*.json"]
|
|
37
30
|
|
|
38
31
|
[tool.setuptools.exclude-package-data]
|
|
39
|
-
"*" = [
|
|
40
|
-
"venv*",
|
|
41
|
-
".env*",
|
|
42
|
-
"*.log",
|
|
43
|
-
".pytest_cache*",
|
|
44
|
-
"tests*",
|
|
45
|
-
]
|
|
32
|
+
"*" = ["venv*", ".env*", "*.log", ".pytest_cache*", "tests*"]
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from foundry.constants import console, TIER_HIERARCHY
|
|
2
|
-
from foundry.utils import get_templates, get_template_info
|
|
3
|
-
from foundry.auth import get_current_license_info
|
|
4
|
-
from rich.panel import Panel
|
|
5
|
-
from rich.tree import Tree
|
|
6
|
-
import questionary
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _get_template_tech(tmpl):
|
|
10
|
-
"""Get technology stack for a template."""
|
|
11
|
-
if (tmpl / "pyproject.toml").exists():
|
|
12
|
-
return "Python (FastAPI/NiceGUI)"
|
|
13
|
-
elif (tmpl / "Gemfile").exists():
|
|
14
|
-
return "Ruby on Rails"
|
|
15
|
-
elif (tmpl / "package.json").exists():
|
|
16
|
-
return "React/Vite"
|
|
17
|
-
elif (tmpl / "build.gradle.kts").exists():
|
|
18
|
-
return "Android (Kotlin/Compose)"
|
|
19
|
-
elif (tmpl / "StackApp").exists():
|
|
20
|
-
return "iOS (Swift/SwiftUI)"
|
|
21
|
-
elif (tmpl / "main.tf").exists():
|
|
22
|
-
return "Terraform"
|
|
23
|
-
elif (tmpl / "astro.config.mjs").exists():
|
|
24
|
-
return "Astro (Static Site)"
|
|
25
|
-
return "Unknown"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _render_interactive_tree(buckets, expansion_state):
|
|
29
|
-
"""Render an interactive tree view with collapsible categories."""
|
|
30
|
-
tree = Tree("[bold green]Seed & Source Inventory[/bold green]")
|
|
31
|
-
|
|
32
|
-
for category, items in buckets.items():
|
|
33
|
-
if not items:
|
|
34
|
-
continue
|
|
35
|
-
|
|
36
|
-
count = len(items)
|
|
37
|
-
is_expanded = expansion_state.get(category, True)
|
|
38
|
-
toggle_icon = "▼" if is_expanded else "►"
|
|
39
|
-
|
|
40
|
-
cat_node = tree.add(f"[bold yellow]{toggle_icon} {category} ({count})[/bold yellow]")
|
|
41
|
-
|
|
42
|
-
if is_expanded:
|
|
43
|
-
for item in items:
|
|
44
|
-
node = cat_node.add(f"[bold cyan]{item['display']}[/bold cyan]")
|
|
45
|
-
tmpl = item['path']
|
|
46
|
-
tech = _get_template_tech(tmpl)
|
|
47
|
-
node.add(f"[dim]{tech}[/dim]")
|
|
48
|
-
else:
|
|
49
|
-
cat_node.add("[dim]Click to expand...[/dim]")
|
|
50
|
-
|
|
51
|
-
console.print(Panel(tree, title="Available Templates", border_style="green"))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def run_explore():
|
|
55
|
-
"""Interactive template explorer with interactive collapsible sections using tree view design."""
|
|
56
|
-
# Get user info for tier checking
|
|
57
|
-
user_info = get_current_license_info()
|
|
58
|
-
user_tier = user_info.get("tier", "free")
|
|
59
|
-
user_rank = TIER_HIERARCHY.get(user_tier, 0)
|
|
60
|
-
|
|
61
|
-
# Group templates by category
|
|
62
|
-
buckets = {
|
|
63
|
-
"Backend Services": [],
|
|
64
|
-
"Frontend & UI": [],
|
|
65
|
-
"Mobile App": [],
|
|
66
|
-
"Data Engineering": [],
|
|
67
|
-
"Infrastructure": [],
|
|
68
|
-
"Other": [],
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
templates = get_templates()
|
|
72
|
-
|
|
73
|
-
for tmpl in templates:
|
|
74
|
-
name = tmpl.name
|
|
75
|
-
# Fetch metadata to get the tier
|
|
76
|
-
info = get_template_info(name)
|
|
77
|
-
tmpl_tier = info.get("tier", "free").lower()
|
|
78
|
-
tmpl_rank = TIER_HIERARCHY.get(tmpl_tier, 0)
|
|
79
|
-
|
|
80
|
-
# Format display name with access status badge
|
|
81
|
-
tmpl_display_name = name
|
|
82
|
-
if tmpl_rank > user_rank:
|
|
83
|
-
tmpl_display_name = f"[dim]{name} [red](🔐 {tmpl_tier.upper()})[/dim]"
|
|
84
|
-
elif tmpl_tier != "free":
|
|
85
|
-
tmpl_display_name = f"{name} [green](✅ {tmpl_tier.upper()})[/green]"
|
|
86
|
-
|
|
87
|
-
# Store template data for processing
|
|
88
|
-
tmpl_data = {"name": name, "display": tmpl_display_name, "path": tmpl}
|
|
89
|
-
|
|
90
|
-
if name in ["python-saas", "rails-api"]:
|
|
91
|
-
buckets["Backend Services"].append(tmpl_data)
|
|
92
|
-
elif name in ["react-client", "rails-ui-kit", "static-landing"]:
|
|
93
|
-
buckets["Frontend & UI"].append(tmpl_data)
|
|
94
|
-
elif name.startswith("mobile-"):
|
|
95
|
-
buckets["Mobile App"].append(tmpl_data)
|
|
96
|
-
elif name == "data-pipeline":
|
|
97
|
-
buckets["Data Engineering"].append(tmpl_data)
|
|
98
|
-
elif name == "terraform-infra":
|
|
99
|
-
buckets["Infrastructure"].append(tmpl_data)
|
|
100
|
-
else:
|
|
101
|
-
buckets["Other"].append(tmpl_data)
|
|
102
|
-
|
|
103
|
-
# Remove empty categories
|
|
104
|
-
buckets = {k: v for k, v in buckets.items() if v}
|
|
105
|
-
|
|
106
|
-
# Track expansion state for each category (all start expanded)
|
|
107
|
-
expansion_state = {category: True for category in buckets.keys()}
|
|
108
|
-
|
|
109
|
-
# Interactive menu loop
|
|
110
|
-
while True:
|
|
111
|
-
# Display the interactive tree
|
|
112
|
-
console.clear()
|
|
113
|
-
_render_interactive_tree(buckets, expansion_state)
|
|
114
|
-
|
|
115
|
-
# Build menu choices for toggling categories
|
|
116
|
-
choices = []
|
|
117
|
-
for category in buckets.keys():
|
|
118
|
-
is_expanded = expansion_state.get(category, True)
|
|
119
|
-
icon = "▼" if is_expanded else "►"
|
|
120
|
-
count = len(buckets[category])
|
|
121
|
-
action = "Collapse" if is_expanded else "Expand"
|
|
122
|
-
choices.append(f"{icon} {action} {category} ({count})")
|
|
123
|
-
|
|
124
|
-
choices.append("🔄 Expand All")
|
|
125
|
-
choices.append("🔒 Collapse All")
|
|
126
|
-
choices.append("Exit")
|
|
127
|
-
|
|
128
|
-
selection = questionary.select(
|
|
129
|
-
"[bold]Toggle Categories[/bold]",
|
|
130
|
-
choices=choices,
|
|
131
|
-
).ask()
|
|
132
|
-
|
|
133
|
-
if selection is None or selection == "Exit":
|
|
134
|
-
break
|
|
135
|
-
elif selection == "🔄 Expand All":
|
|
136
|
-
expansion_state = {k: True for k in expansion_state.keys()}
|
|
137
|
-
elif selection == "🔒 Collapse All":
|
|
138
|
-
expansion_state = {k: False for k in expansion_state.keys()}
|
|
139
|
-
else:
|
|
140
|
-
# Extract category name from choice
|
|
141
|
-
for category in buckets.keys():
|
|
142
|
-
if category in selection:
|
|
143
|
-
expansion_state[category] = not expansion_state[category]
|
|
144
|
-
break
|
|
145
|
-
input("[dim]Press Enter to continue...[/dim]")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|