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.
Files changed (94) hide show
  1. {sscli-2.0.2 → sscli-2.1.0}/PKG-INFO +1 -1
  2. sscli-2.1.0/foundry/actions/development.py +397 -0
  3. sscli-2.1.0/foundry/actions/explore.py +139 -0
  4. {sscli-2.0.2 → sscli-2.1.0}/foundry/cli.py +53 -8
  5. {sscli-2.0.2 → sscli-2.1.0}/foundry/interactive.py +9 -7
  6. {sscli-2.0.2 → sscli-2.1.0}/pyproject.toml +7 -20
  7. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/PKG-INFO +1 -1
  8. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/SOURCES.txt +1 -0
  9. sscli-2.0.2/foundry/actions/explore.py +0 -145
  10. {sscli-2.0.2 → sscli-2.1.0}/README.md +0 -0
  11. {sscli-2.0.2 → sscli-2.1.0}/foundry/__init__.py +0 -0
  12. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/__init__.py +0 -0
  13. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/account_info.py +0 -0
  14. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/documentation.py +0 -0
  15. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/features.py +0 -0
  16. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/generation_utils.py +0 -0
  17. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/generator.py +0 -0
  18. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/health.py +0 -0
  19. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/interactive_config.py +0 -0
  20. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/metadata.py +0 -0
  21. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/repo_utils.py +0 -0
  22. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/setup.py +0 -0
  23. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/validation.py +0 -0
  24. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/__init__.py +0 -0
  25. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/models.py +0 -0
  26. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/reports.py +0 -0
  27. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verification/verifier.py +0 -0
  28. {sscli-2.0.2 → sscli-2.1.0}/foundry/actions/verify.py +0 -0
  29. {sscli-2.0.2 → sscli-2.1.0}/foundry/admin_tools.py +0 -0
  30. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/__init__.py +0 -0
  31. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/access_manager.py +0 -0
  32. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/manager.py +0 -0
  33. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/models.py +0 -0
  34. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha/view.py +0 -0
  35. {sscli-2.0.2 → sscli-2.1.0}/foundry/alpha_expiration.py +0 -0
  36. {sscli-2.0.2 → sscli-2.1.0}/foundry/animated_experience.py +0 -0
  37. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/__init__.py +0 -0
  38. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/animation.py +0 -0
  39. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/assets.py +0 -0
  40. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/base.py +0 -0
  41. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/canvas.py +0 -0
  42. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/components.py +0 -0
  43. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/composites.py +0 -0
  44. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/core.py +0 -0
  45. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/__init__.py +0 -0
  46. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/animation_engine.py +0 -0
  47. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/clock.py +0 -0
  48. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/frame_buffer.py +0 -0
  49. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/engine/state.py +0 -0
  50. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/rendering.py +0 -0
  51. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/__init__.py +0 -0
  52. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/entrance.py +0 -0
  53. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/forge.py +0 -0
  54. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/info_slide.py +0 -0
  55. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_disperse.py +0 -0
  56. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_dissolve.py +0 -0
  57. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_reveal.py +0 -0
  58. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/logo_slide.py +0 -0
  59. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/rain.py +0 -0
  60. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/scenes/tranquility.py +0 -0
  61. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/sections/__init__.py +0 -0
  62. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/ui/__init__.py +0 -0
  63. {sscli-2.0.2 → sscli-2.1.0}/foundry/animations/utils.py +0 -0
  64. {sscli-2.0.2 → sscli-2.1.0}/foundry/auth.py +0 -0
  65. {sscli-2.0.2 → sscli-2.1.0}/foundry/client_access_manager.py +0 -0
  66. {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/__init__.py +0 -0
  67. {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/auth.py +0 -0
  68. {sscli-2.0.2 → sscli-2.1.0}/foundry/commands/interactive.py +0 -0
  69. {sscli-2.0.2 → sscli-2.1.0}/foundry/constants.py +0 -0
  70. {sscli-2.0.2 → sscli-2.1.0}/foundry/development_experience.py +0 -0
  71. {sscli-2.0.2 → sscli-2.1.0}/foundry/gate.py +0 -0
  72. {sscli-2.0.2 → sscli-2.1.0}/foundry/interactive_utils.py +0 -0
  73. {sscli-2.0.2 → sscli-2.1.0}/foundry/internal_experience.py +0 -0
  74. {sscli-2.0.2 → sscli-2.1.0}/foundry/manage.py +0 -0
  75. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/__init__.py +0 -0
  76. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/cli.py +0 -0
  77. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/licenses.py +0 -0
  78. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/manager.py +0 -0
  79. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/menus.py +0 -0
  80. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/reporter.py +0 -0
  81. {sscli-2.0.2 → sscli-2.1.0}/foundry/ops/runner.py +0 -0
  82. {sscli-2.0.2 → sscli-2.1.0}/foundry/patch.py +0 -0
  83. {sscli-2.0.2 → sscli-2.1.0}/foundry/telemetry.py +0 -0
  84. {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/__init__.py +0 -0
  85. {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/client/__init__.py +0 -0
  86. {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/development/__init__.py +0 -0
  87. {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/development/build_tools.py +0 -0
  88. {sscli-2.0.2 → sscli-2.1.0}/foundry/tools/internal/__init__.py +0 -0
  89. {sscli-2.0.2 → sscli-2.1.0}/foundry/utils.py +0 -0
  90. {sscli-2.0.2 → sscli-2.1.0}/setup.cfg +0 -0
  91. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/dependency_links.txt +0 -0
  92. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/entry_points.txt +0 -0
  93. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/requires.txt +0 -0
  94. {sscli-2.0.2 → sscli-2.1.0}/sscli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sscli
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: Seed & Source CLI - Multi-tenant SaaS scaffolding with Seed & Source Core
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -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=True,
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
- @app.command("docs")
140
- def docs():
141
- """List available documentation."""
142
- log_event("COMMAND", "docs")
143
- run_view_docs(interactive=False)
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 == "docs":
129
- choice = "View Documentation"
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 == "View Documentation":
169
- run_view_docs(interactive=True)
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.2"
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.packages.find]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sscli
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: Seed & Source CLI - Multi-tenant SaaS scaffolding with Seed & Source Core
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -19,6 +19,7 @@ foundry/telemetry.py
19
19
  foundry/utils.py
20
20
  foundry/actions/__init__.py
21
21
  foundry/actions/account_info.py
22
+ foundry/actions/development.py
22
23
  foundry/actions/documentation.py
23
24
  foundry/actions/explore.py
24
25
  foundry/actions/features.py
@@ -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