sscli 0.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-0.1.0/PKG-INFO +15 -0
- sscli-0.1.0/README.md +2 -0
- sscli-0.1.0/foundry/__init__.py +0 -0
- sscli-0.1.0/foundry/alpha_expiration.py +46 -0
- sscli-0.1.0/foundry/animated_experience.py +190 -0
- sscli-0.1.0/foundry/auth.py +120 -0
- sscli-0.1.0/foundry/cli.py +117 -0
- sscli-0.1.0/foundry/constants.py +128 -0
- sscli-0.1.0/foundry/interactive.py +151 -0
- sscli-0.1.0/foundry/interactive_utils.py +81 -0
- sscli-0.1.0/foundry/manage.py +9 -0
- sscli-0.1.0/foundry/telemetry.py +23 -0
- sscli-0.1.0/foundry/utils.py +66 -0
- sscli-0.1.0/pyproject.toml +30 -0
- sscli-0.1.0/setup.cfg +4 -0
- sscli-0.1.0/sscli.egg-info/PKG-INFO +15 -0
- sscli-0.1.0/sscli.egg-info/SOURCES.txt +19 -0
- sscli-0.1.0/sscli.egg-info/dependency_links.txt +1 -0
- sscli-0.1.0/sscli.egg-info/entry_points.txt +2 -0
- sscli-0.1.0/sscli.egg-info/requires.txt +6 -0
- sscli-0.1.0/sscli.egg-info/top_level.txt +1 -0
sscli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sscli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Seed & Source CLI
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer[all]
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: questionary
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: pathlib
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
13
|
+
|
|
14
|
+
# Seed & Source CLI
|
|
15
|
+
Unified interface for Stack Foundry templates.
|
sscli-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Alpha Package Expiration Check (Refactored)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from .alpha import AlphaInfo, ExpirationStatus, AlphaExpirationManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_expiration_manager() -> AlphaExpirationManager:
|
|
11
|
+
"""Factory function to get the global expiration manager."""
|
|
12
|
+
return AlphaExpirationManager()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""Debug/test CLI for expiration checks."""
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
parser = argparse.ArgumentParser(description="Alpha Package Expiration Check")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--status", action="store_true", help="Show alpha status (if available)"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--check", type=str, help="Check if action is permitted (e.g., 'generate')"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
manager = AlphaExpirationManager()
|
|
29
|
+
|
|
30
|
+
if args.status:
|
|
31
|
+
manager.show_status()
|
|
32
|
+
elif args.check:
|
|
33
|
+
allowed = manager.check_before_action(args.check)
|
|
34
|
+
sys.exit(0 if allowed else 1)
|
|
35
|
+
else:
|
|
36
|
+
if manager.is_alpha():
|
|
37
|
+
manager.show_status()
|
|
38
|
+
else:
|
|
39
|
+
from rich.console import Console
|
|
40
|
+
|
|
41
|
+
Console().print("[dim]Not running in alpha mode[/dim]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
46
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from foundry.constants import console
|
|
2
|
+
from foundry.actions.documentation import run_view_docs
|
|
3
|
+
from foundry.actions.explore import run_explore
|
|
4
|
+
from foundry.actions.generator import run_generator
|
|
5
|
+
from foundry.actions.validation import run_smoke_tests
|
|
6
|
+
from foundry.interactive_utils import manage_config_menu, pause
|
|
7
|
+
from foundry.utils import is_internal_dev
|
|
8
|
+
|
|
9
|
+
import questionary
|
|
10
|
+
|
|
11
|
+
from typing import cast, List
|
|
12
|
+
from foundry.animations.base import Animation
|
|
13
|
+
|
|
14
|
+
# Animation Imports - moved inside function to avoid unbound variables
|
|
15
|
+
# try:
|
|
16
|
+
# from foundry.animations import (AnimationSequence,
|
|
17
|
+
# InteractiveAnimationEngine)
|
|
18
|
+
# from foundry.animations.scenes import (LogoDisperseAnimation,
|
|
19
|
+
# LogoRevealAnimation,
|
|
20
|
+
# LogoStaticAnimation,
|
|
21
|
+
# TranquilityAnimation)
|
|
22
|
+
|
|
23
|
+
# HAS_ANIMATIONS = True
|
|
24
|
+
# except Exception:
|
|
25
|
+
# HAS_ANIMATIONS = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_animated_experience():
|
|
29
|
+
"""Plays the full robust animation sequence and handles user choice loop."""
|
|
30
|
+
# Import animations here to avoid unbound variables
|
|
31
|
+
try:
|
|
32
|
+
from foundry.animations import (AnimationSequence,
|
|
33
|
+
InteractiveAnimationEngine)
|
|
34
|
+
from foundry.animations.scenes import (LogoDisperseAnimation,
|
|
35
|
+
LogoRevealAnimation,
|
|
36
|
+
LogoStaticAnimation,
|
|
37
|
+
TranquilityAnimation,
|
|
38
|
+
ForgeAnimation)
|
|
39
|
+
except Exception:
|
|
40
|
+
console.print("[bold red]Animation system not fully loaded.[/]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Internal Mode selection for Developers
|
|
44
|
+
if is_internal_dev():
|
|
45
|
+
mode = questionary.select(
|
|
46
|
+
"Select Your Experience:",
|
|
47
|
+
choices=[
|
|
48
|
+
"Seed & Source (Client Side)",
|
|
49
|
+
"Foundry Ops (Internal Side)",
|
|
50
|
+
"Exit",
|
|
51
|
+
],
|
|
52
|
+
style=questionary.Style([("highlighted", "fg:#00ffff bold")]),
|
|
53
|
+
).ask()
|
|
54
|
+
|
|
55
|
+
if mode == "Exit" or mode is None:
|
|
56
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if mode == "Foundry Ops (Internal Side)":
|
|
60
|
+
run_internal_animated_experience()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
engine = InteractiveAnimationEngine(fps=60, console=console)
|
|
64
|
+
|
|
65
|
+
# 1. Play Intro Sequence once
|
|
66
|
+
try:
|
|
67
|
+
intro = AnimationSequence(
|
|
68
|
+
cast(List[Animation], [
|
|
69
|
+
LogoRevealAnimation(duration=1.2),
|
|
70
|
+
LogoStaticAnimation(duration=1.0),
|
|
71
|
+
LogoDisperseAnimation(duration=2.0),
|
|
72
|
+
])
|
|
73
|
+
)
|
|
74
|
+
engine.play(intro)
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
77
|
+
return
|
|
78
|
+
except Exception as e:
|
|
79
|
+
console.print(f"[red]Error during intro: {e}[/]")
|
|
80
|
+
|
|
81
|
+
# 2. Main Menu Loop
|
|
82
|
+
while True:
|
|
83
|
+
try:
|
|
84
|
+
# Play Tranquility with menu until a choice is made
|
|
85
|
+
choice = engine.play(TranquilityAnimation())
|
|
86
|
+
|
|
87
|
+
if not choice or choice == "Exit":
|
|
88
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
# Dispatch the choice
|
|
92
|
+
if choice == "Explore Templates":
|
|
93
|
+
run_explore()
|
|
94
|
+
pause()
|
|
95
|
+
elif choice == "Generate Project":
|
|
96
|
+
run_generator(interactive=True)
|
|
97
|
+
pause()
|
|
98
|
+
elif choice == "Manage Config":
|
|
99
|
+
manage_config_menu()
|
|
100
|
+
elif choice == "System Validation":
|
|
101
|
+
run_smoke_tests()
|
|
102
|
+
pause()
|
|
103
|
+
elif choice == "View Docs":
|
|
104
|
+
run_view_docs(interactive=True)
|
|
105
|
+
pause()
|
|
106
|
+
|
|
107
|
+
except KeyboardInterrupt:
|
|
108
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
109
|
+
break
|
|
110
|
+
except Exception as e:
|
|
111
|
+
console.print(f"[red]Error during menu: {e}[/]")
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_internal_animated_experience():
|
|
116
|
+
"""Plays the internal development animation sequence with industrial theme."""
|
|
117
|
+
# Import animations here to avoid unbound variables
|
|
118
|
+
try:
|
|
119
|
+
from foundry.animations import (AnimationSequence,
|
|
120
|
+
InteractiveAnimationEngine)
|
|
121
|
+
from foundry.animations.scenes import (LogoDisperseAnimation,
|
|
122
|
+
LogoRevealAnimation,
|
|
123
|
+
LogoStaticAnimation,
|
|
124
|
+
ForgeAnimation)
|
|
125
|
+
except Exception:
|
|
126
|
+
console.print("[bold red]Animation system not fully loaded.[/]")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
engine = InteractiveAnimationEngine(fps=60, console=console)
|
|
130
|
+
|
|
131
|
+
# 1. Play Intro Sequence once (industrial theme)
|
|
132
|
+
try:
|
|
133
|
+
intro = AnimationSequence(
|
|
134
|
+
[
|
|
135
|
+
LogoRevealAnimation(duration=1.2),
|
|
136
|
+
LogoStaticAnimation(duration=1.0),
|
|
137
|
+
LogoDisperseAnimation(duration=2.0),
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
engine.play(intro)
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
143
|
+
return
|
|
144
|
+
except Exception as e:
|
|
145
|
+
console.print(f"[red]Error during intro: {e}[/]")
|
|
146
|
+
|
|
147
|
+
# 2. Main Menu Loop with Forge theme
|
|
148
|
+
while True:
|
|
149
|
+
try:
|
|
150
|
+
# Play Forge animation with industrial menu
|
|
151
|
+
choice = engine.play(ForgeAnimation())
|
|
152
|
+
|
|
153
|
+
if not choice or choice == "Back to Main":
|
|
154
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Dispatch the choice for internal ops
|
|
158
|
+
try:
|
|
159
|
+
from foundry.ops.manager import InternalManager
|
|
160
|
+
from foundry.ops.runner import run_verification_script
|
|
161
|
+
manager = InternalManager()
|
|
162
|
+
|
|
163
|
+
if choice == "Verify Builds":
|
|
164
|
+
manager.verify_builds()
|
|
165
|
+
pause()
|
|
166
|
+
elif choice == "Hygiene Check":
|
|
167
|
+
run_verification_script(manager.root_dir, "verify_hygiene", "Code Hygiene (File Lengths)")
|
|
168
|
+
pause()
|
|
169
|
+
elif choice == "Full Suite":
|
|
170
|
+
manager.verify_full_suite()
|
|
171
|
+
pause()
|
|
172
|
+
elif choice == "Clean Reports":
|
|
173
|
+
manager.clean_reports()
|
|
174
|
+
pause()
|
|
175
|
+
elif choice == "View Docs":
|
|
176
|
+
run_view_docs(interactive=True)
|
|
177
|
+
pause()
|
|
178
|
+
elif choice == "Back to Main":
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
except ImportError:
|
|
182
|
+
console.print("[red]Ops manager not available in this build.[/]")
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
except KeyboardInterrupt:
|
|
186
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
187
|
+
break
|
|
188
|
+
except Exception as e:
|
|
189
|
+
console.print(f"[red]Error during menu: {e}[/]")
|
|
190
|
+
break
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from foundry.constants import console, API_BASE_URL
|
|
9
|
+
|
|
10
|
+
# Configuration
|
|
11
|
+
# The base URL is managed in foundry/constants.py (configurable via environment variables)
|
|
12
|
+
LICENSE_SERVER = API_BASE_URL
|
|
13
|
+
CREDENTIALS_FILE = Path.home() / ".config" / "sscli" / "credentials.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_access_token() -> Optional[str]:
|
|
17
|
+
"""Retrieve the stored access token if it exists."""
|
|
18
|
+
if not CREDENTIALS_FILE.exists():
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
with open(CREDENTIALS_FILE, "r") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
return data.get("access_token")
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_credentials(data: Dict[str, Any]) -> None:
|
|
29
|
+
"""Save user credentials to the home directory."""
|
|
30
|
+
CREDENTIALS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
with open(CREDENTIALS_FILE, "w") as f:
|
|
32
|
+
json.dump(data, f, indent=2)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def login_flow() -> None:
|
|
36
|
+
"""Execute the GitHub Device Flow via the License Server."""
|
|
37
|
+
console.print("Initiating login with GitHub...")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
response = requests.post(f"{LICENSE_SERVER}/auth/device/code")
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
console.print(f"Error connecting to License Server at {LICENSE_SERVER}: {e}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
data = response.json()
|
|
47
|
+
device_code = data["device_code"]
|
|
48
|
+
user_code = data["user_code"]
|
|
49
|
+
verification_uri = data["verification_uri"]
|
|
50
|
+
interval = data.get("interval", 5)
|
|
51
|
+
|
|
52
|
+
console.print(f"\n1. Go to: {verification_uri}")
|
|
53
|
+
console.print(f"2. Enter code: {user_code}\n")
|
|
54
|
+
console.print("Waiting for authorization...")
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
try:
|
|
58
|
+
token_resp = requests.get(
|
|
59
|
+
f"{LICENSE_SERVER}/auth/device/token",
|
|
60
|
+
params={"device_code": device_code},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if token_resp.status_code == 200:
|
|
64
|
+
credentials = token_resp.json()
|
|
65
|
+
save_credentials(credentials)
|
|
66
|
+
console.print(
|
|
67
|
+
f"\nSuccessfully logged in as {credentials.get('email', 'User')}!"
|
|
68
|
+
)
|
|
69
|
+
console.print(
|
|
70
|
+
f"License Tier: {credentials.get('tier', 'free').upper()}"
|
|
71
|
+
)
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if token_resp.status_code == 428: # Pending
|
|
75
|
+
time.sleep(interval)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# If we get here, something went wrong
|
|
79
|
+
console.print(
|
|
80
|
+
f"Login failed: {token_resp.json().get('detail', 'Unknown error')}"
|
|
81
|
+
)
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
console.print(f"Polling error: {e}")
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_current_license_info() -> Dict[str, Any]:
|
|
90
|
+
"""Read the current license/tier info from local storage.
|
|
91
|
+
|
|
92
|
+
Returns a dict with at least `tier` and `authorized` keys when no
|
|
93
|
+
credentials are present or when reading fails.
|
|
94
|
+
"""
|
|
95
|
+
if not CREDENTIALS_FILE.exists():
|
|
96
|
+
return {"tier": "free", "authorized": False}
|
|
97
|
+
try:
|
|
98
|
+
with open(CREDENTIALS_FILE, "r") as f:
|
|
99
|
+
data = json.load(f)
|
|
100
|
+
if isinstance(data, dict):
|
|
101
|
+
return data
|
|
102
|
+
# If stored data is malformed, return a sensible default
|
|
103
|
+
return {"tier": "free", "authorized": False}
|
|
104
|
+
except Exception:
|
|
105
|
+
return {"tier": "free", "authorized": False}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def logout() -> None:
|
|
109
|
+
"""Clear local stored credentials (logout).
|
|
110
|
+
|
|
111
|
+
Removes the `credentials.json` file if present and reports status to the console.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
if CREDENTIALS_FILE.exists():
|
|
115
|
+
CREDENTIALS_FILE.unlink()
|
|
116
|
+
console.print("[green]Logged out:[/] removed local credentials.")
|
|
117
|
+
else:
|
|
118
|
+
console.print("[yellow]No credentials found to remove.[/yellow]")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
console.print(f"[red]Error clearing credentials:[/] {e}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from foundry.constants import console
|
|
3
|
+
import typer
|
|
4
|
+
from foundry.actions.documentation import run_view_docs
|
|
5
|
+
from foundry.actions.explore import run_explore
|
|
6
|
+
from foundry.actions.generator import run_generator
|
|
7
|
+
from foundry.actions.health import run_health_check
|
|
8
|
+
from foundry.actions.setup import run_setup
|
|
9
|
+
from foundry.actions.validation import run_smoke_tests
|
|
10
|
+
from foundry.actions.verify import run_verification
|
|
11
|
+
from foundry.auth import get_current_license_info
|
|
12
|
+
from foundry.ops.cli import ops_app
|
|
13
|
+
from foundry.telemetry import log_event
|
|
14
|
+
from foundry.commands.auth import auth_app, run_whoami
|
|
15
|
+
from foundry.commands.interactive import interactive_app
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Stack Foundry CLI Tool")
|
|
18
|
+
|
|
19
|
+
# Add sub-apps from separate modules
|
|
20
|
+
app.add_typer(auth_app, name="auth")
|
|
21
|
+
app.add_typer(interactive_app, name="interactive")
|
|
22
|
+
app.add_typer(ops_app, name="ops", help="Internal operations and maintenance.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("whoami")
|
|
26
|
+
def whoami():
|
|
27
|
+
run_whoami()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("explore")
|
|
32
|
+
def explore():
|
|
33
|
+
"""List available templates."""
|
|
34
|
+
log_event("COMMAND", "explore")
|
|
35
|
+
run_explore()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("new")
|
|
39
|
+
def new(
|
|
40
|
+
template: str = typer.Option(..., help="Name of the template to use"),
|
|
41
|
+
name: str = typer.Option(..., help="Name of the application"),
|
|
42
|
+
target: Optional[str] = typer.Option(
|
|
43
|
+
None, help="Target directory (defaults to name)"
|
|
44
|
+
),
|
|
45
|
+
no_lint: bool = typer.Option(False, "--no-lint", help="Disable default linters"),
|
|
46
|
+
secrets: str = typer.Option(
|
|
47
|
+
"Dotenv (Standard .env files)", help="Secrets management strategy"
|
|
48
|
+
),
|
|
49
|
+
):
|
|
50
|
+
"""Generate a new project from a template."""
|
|
51
|
+
log_event("COMMAND", f"new --template {template} --name {name}")
|
|
52
|
+
run_generator(
|
|
53
|
+
template_name=template,
|
|
54
|
+
app_name=name,
|
|
55
|
+
target_dir_name=target,
|
|
56
|
+
use_linters=not no_lint,
|
|
57
|
+
interactive=False,
|
|
58
|
+
secrets_strategy=secrets,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("setup")
|
|
63
|
+
def setup(
|
|
64
|
+
verbose: bool = typer.Option(
|
|
65
|
+
False, "--verbose", "-v", help="Enable detailed logging"
|
|
66
|
+
),
|
|
67
|
+
force: bool = typer.Option(
|
|
68
|
+
False, "--force", help="Force setup even if already configured"
|
|
69
|
+
),
|
|
70
|
+
skip_deps: bool = typer.Option(
|
|
71
|
+
False, "--skip-deps", help="Skip dependency installation"
|
|
72
|
+
),
|
|
73
|
+
env: str = typer.Option("dev", "--env", help="Environment to setup (default: dev)"),
|
|
74
|
+
strategy: Optional[str] = typer.Option(
|
|
75
|
+
None, "--strategy", help="Setup strategy (docker, etc)"
|
|
76
|
+
),
|
|
77
|
+
):
|
|
78
|
+
"""Run setup for templates."""
|
|
79
|
+
log_event("COMMAND", f"setup --env {env}")
|
|
80
|
+
run_setup(
|
|
81
|
+
verbose=verbose, force=force, skip_deps=skip_deps, env=env, strategy=strategy
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("validate")
|
|
86
|
+
def validate():
|
|
87
|
+
"""Run smoke tests."""
|
|
88
|
+
log_event("COMMAND", "validate")
|
|
89
|
+
run_smoke_tests()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command("health")
|
|
93
|
+
def health():
|
|
94
|
+
"""Check configuration and health of templates."""
|
|
95
|
+
log_event("COMMAND", "health")
|
|
96
|
+
run_health_check()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("verify")
|
|
100
|
+
def verify(
|
|
101
|
+
skip_docker: bool = typer.Option(
|
|
102
|
+
False, "--skip-docker", help="Skip Docker builds (faster, less thorough)"
|
|
103
|
+
),
|
|
104
|
+
no_reports: bool = typer.Option(
|
|
105
|
+
False, "--no-reports", help="Skip generating CSV/MD reports"
|
|
106
|
+
),
|
|
107
|
+
):
|
|
108
|
+
"""Verify template integrity and buildability."""
|
|
109
|
+
log_event("COMMAND", "verify")
|
|
110
|
+
run_verification(include_docker=not skip_docker, output_reports=not no_reports)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("docs")
|
|
114
|
+
def docs():
|
|
115
|
+
"""List available documentation."""
|
|
116
|
+
log_event("COMMAND", "docs")
|
|
117
|
+
run_view_docs(interactive=False)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent
|
|
7
|
+
|
|
8
|
+
# Configurable base URLs - can be overridden via environment variables
|
|
9
|
+
# For Render Free Tier, you can set LICENSE_SERVER_URL directly
|
|
10
|
+
SEED_SOURCE_DOMAIN = os.getenv("SEED_SOURCE_DOMAIN", "seedandsource.dev")
|
|
11
|
+
# DEFAULT_API_URL updated for current Alpha deployment on Render
|
|
12
|
+
DEFAULT_API_URL = "https://license-server-53oj.onrender.com/api/v1"
|
|
13
|
+
API_BASE_URL = os.getenv("LICENSE_SERVER_URL", DEFAULT_API_URL)
|
|
14
|
+
FORMS_BASE_URL = os.getenv("FORMS_BASE_URL", f"https://forms.{SEED_SOURCE_DOMAIN}")
|
|
15
|
+
PRICING_URL = os.getenv("PRICING_URL", f"https://{SEED_SOURCE_DOMAIN}/pricing")
|
|
16
|
+
SALES_EMAIL = f"sales@{SEED_SOURCE_DOMAIN}"
|
|
17
|
+
ALPHA_SUPPORT_EMAIL = f"alpha-support@{SEED_SOURCE_DOMAIN}"
|
|
18
|
+
ALPHA_FEEDBACK_FORM = f"{FORMS_BASE_URL}/alpha"
|
|
19
|
+
|
|
20
|
+
# Metadata-based template registry for PyPI distribution
|
|
21
|
+
# This allows the CLI to know about templates without them being on disk
|
|
22
|
+
TEMPLATE_REGISTRY = {
|
|
23
|
+
"python-saas": {
|
|
24
|
+
"name": "Python SaaS Boilerplate",
|
|
25
|
+
"description": "Hexagonal Architecture (FastAPI + SQLAlchemy)",
|
|
26
|
+
"tier": "free",
|
|
27
|
+
"repo": "https://github.com/seed-source/python-saas.git",
|
|
28
|
+
},
|
|
29
|
+
"rails-api": {
|
|
30
|
+
"name": "Rails API Suite",
|
|
31
|
+
"description": "JSON API with Devise, RSpec, and Docker",
|
|
32
|
+
"tier": "free",
|
|
33
|
+
"repo": "https://github.com/seed-source/rails-api.git",
|
|
34
|
+
},
|
|
35
|
+
"react-client": {
|
|
36
|
+
"name": "React Vite Client",
|
|
37
|
+
"description": "Modern frontend with Tailwind and React Query",
|
|
38
|
+
"tier": "free",
|
|
39
|
+
"repo": "https://github.com/seed-source/react-client.git",
|
|
40
|
+
},
|
|
41
|
+
"rails-ui-kit": {
|
|
42
|
+
"name": "Rails UI Kit",
|
|
43
|
+
"description": "Full-stack Rails with ViewComponent and Tailwind",
|
|
44
|
+
"tier": "alpha",
|
|
45
|
+
"repo": "https://github.com/seed-source/rails-ui-kit-private.git",
|
|
46
|
+
},
|
|
47
|
+
"data-pipeline": {
|
|
48
|
+
"name": "Data Pipeline (dbt)",
|
|
49
|
+
"description": "Modern data stack with dbt and Python",
|
|
50
|
+
"tier": "alpha",
|
|
51
|
+
"repo": "https://github.com/seed-source/data-pipeline-private.git",
|
|
52
|
+
},
|
|
53
|
+
"mobile-android": {
|
|
54
|
+
"name": "Mobile Android",
|
|
55
|
+
"description": "Kotlin-based Android application",
|
|
56
|
+
"tier": "alpha",
|
|
57
|
+
"repo": "https://github.com/seed-source/mobile-android-private.git",
|
|
58
|
+
},
|
|
59
|
+
"mobile-ios": {
|
|
60
|
+
"name": "Mobile iOS",
|
|
61
|
+
"description": "SwiftUI-based iOS application",
|
|
62
|
+
"tier": "alpha",
|
|
63
|
+
"repo": "https://github.com/seed-source/mobile-ios-private.git",
|
|
64
|
+
},
|
|
65
|
+
"terraform-infra": {
|
|
66
|
+
"name": "Terraform Infrastructure",
|
|
67
|
+
"description": "Multi-cloud IaC modules",
|
|
68
|
+
"tier": "alpha",
|
|
69
|
+
"repo": "https://github.com/seed-source/terraform-infra-private.git",
|
|
70
|
+
},
|
|
71
|
+
"wiring": {
|
|
72
|
+
"name": "Docker Wiring",
|
|
73
|
+
"description": "Orchestration for multi-service stacks",
|
|
74
|
+
"tier": "free",
|
|
75
|
+
"repo": "https://github.com/seed-source/wiring.git",
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
TIER_HIERARCHY = {"free": 0, "alpha": 1, "pro": 2, "enterprise": 3}
|
|
80
|
+
|
|
81
|
+
console = Console()
|
|
82
|
+
|
|
83
|
+
FOUNDRY_BANNER = """
|
|
84
|
+
[bold cyan]
|
|
85
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
86
|
+
║ ║
|
|
87
|
+
║ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ ║
|
|
88
|
+
║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ║
|
|
89
|
+
║ ███████╗ ██║ ███████║██║ █████╔╝ ║
|
|
90
|
+
║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ ║
|
|
91
|
+
║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ ║
|
|
92
|
+
║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ║
|
|
93
|
+
║ ║
|
|
94
|
+
║ ███████╗ ██████╗ ██╗ ██╗███╗ ██╗██████╗ ██████╗ ██╗ ██╗ ║
|
|
95
|
+
║ ██╔════╝██╔═══██╗██║ ██║████╗ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝ ║
|
|
96
|
+
║ █████╗ ██║ ██║██║ ██║██╔██╗ ██║██║ ██║██████╔╝ ╚████╔╝ ║
|
|
97
|
+
║ ██╔══╝ ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══██╗ ╚██╔╝ ║
|
|
98
|
+
║ ██║ ╚██████╔╝╚██████╔╝██║ ╚████║██████╔╝██║ ██║ ██║ ║
|
|
99
|
+
║ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ║
|
|
100
|
+
║ ║
|
|
101
|
+
╟────────────────────────────────────────────────────────────────────────╢
|
|
102
|
+
║ ║
|
|
103
|
+
║ ⬢ Template Management & Deployment System ║
|
|
104
|
+
║ ║
|
|
105
|
+
║ ├─ 📦 9 Production-Ready Templates ║
|
|
106
|
+
║ ├─ 🏗️ Hexagonal Architecture Patterns ║
|
|
107
|
+
║ ├─ 🐳 Docker-First Deployment ║
|
|
108
|
+
║ ├─ 🔧 Automated Configuration & Setup ║
|
|
109
|
+
║ └─ ✅ Built-in Validation & Smoke Tests ║
|
|
110
|
+
║ ║
|
|
111
|
+
╟────────────────────────────────────────────────────────────────────────╢
|
|
112
|
+
║ ║
|
|
113
|
+
║ STATUS: [green]●[/green] OPERATIONAL │ PATTERN: PORTS & ADAPTERS │ YEAR: 2026 ║
|
|
114
|
+
║ ║
|
|
115
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
116
|
+
[/bold cyan]
|
|
117
|
+
[dim] The Foundry Overseer - Stack Management CLI[/dim]
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Menu configuration
|
|
121
|
+
ANIMATED_MENU_ITEMS = [
|
|
122
|
+
"Explore Templates",
|
|
123
|
+
"Generate Project",
|
|
124
|
+
"Manage Config",
|
|
125
|
+
"System Validation",
|
|
126
|
+
"View Docs",
|
|
127
|
+
"Exit",
|
|
128
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import questionary
|
|
2
|
+
from foundry.actions.documentation import run_view_docs
|
|
3
|
+
from foundry.actions.explore import run_explore
|
|
4
|
+
from foundry.actions.generator import run_generator
|
|
5
|
+
from foundry.actions.validation import run_smoke_tests
|
|
6
|
+
from foundry.alpha_expiration import get_expiration_manager
|
|
7
|
+
from foundry.alpha.view import show_warning_message
|
|
8
|
+
from foundry.auth import get_current_license_info, login_flow
|
|
9
|
+
from foundry.constants import FOUNDRY_BANNER, console
|
|
10
|
+
from foundry.utils import is_internal_dev
|
|
11
|
+
from foundry.interactive_utils import manage_config_menu, pause, internal_ops_menu
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Animation Imports - Standard Menu only needs to know if they exist for banner display
|
|
15
|
+
try:
|
|
16
|
+
from foundry.animations import InteractiveAnimationEngine
|
|
17
|
+
HAS_ANIMATIONS = True
|
|
18
|
+
except Exception:
|
|
19
|
+
HAS_ANIMATIONS = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_animated_experience():
|
|
23
|
+
"""Proxy to the actual animated experience implementation."""
|
|
24
|
+
from foundry.animated_experience import run_animated_experience as _run
|
|
25
|
+
_run()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def interactive_menu():
|
|
29
|
+
if HAS_ANIMATIONS:
|
|
30
|
+
try:
|
|
31
|
+
# No intro sequence for standard menu - jumping straight to banner
|
|
32
|
+
console.print(FOUNDRY_BANNER)
|
|
33
|
+
except Exception:
|
|
34
|
+
console.print(FOUNDRY_BANNER)
|
|
35
|
+
else:
|
|
36
|
+
console.print(FOUNDRY_BANNER)
|
|
37
|
+
|
|
38
|
+
# Check authentication status
|
|
39
|
+
auth_info = get_current_license_info()
|
|
40
|
+
if auth_info.get("tier") == "free":
|
|
41
|
+
console.print("\n[bold yellow]🔐 Authentication Required[/bold yellow]")
|
|
42
|
+
console.print("You need to authenticate to access PRO and ALPHA features.\n")
|
|
43
|
+
|
|
44
|
+
auth_choice = questionary.select(
|
|
45
|
+
"Would you like to authenticate now?",
|
|
46
|
+
choices=[
|
|
47
|
+
"Login with GitHub (Recommended)",
|
|
48
|
+
"Continue as Free User",
|
|
49
|
+
"Exit",
|
|
50
|
+
],
|
|
51
|
+
).ask()
|
|
52
|
+
|
|
53
|
+
if auth_choice == "Exit" or auth_choice is None:
|
|
54
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
55
|
+
return
|
|
56
|
+
elif auth_choice == "Login with GitHub (Recommended)":
|
|
57
|
+
try:
|
|
58
|
+
login_flow()
|
|
59
|
+
# Refresh auth info after login
|
|
60
|
+
auth_info = get_current_license_info()
|
|
61
|
+
console.print("\n[green]✓ Authentication successful![/green]\n")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
console.print(f"\n[red]✗ Authentication failed:[/red] {e}\n")
|
|
64
|
+
console.print("Continuing as free user...\n")
|
|
65
|
+
|
|
66
|
+
# Internal Mode selection for Developers
|
|
67
|
+
if is_internal_dev():
|
|
68
|
+
mode = questionary.select(
|
|
69
|
+
"Select Your Experience:",
|
|
70
|
+
choices=[
|
|
71
|
+
"Seed & Source (Client Side)",
|
|
72
|
+
"Foundry Ops (Internal Side)",
|
|
73
|
+
"Exit",
|
|
74
|
+
],
|
|
75
|
+
style=questionary.Style([("highlighted", "fg:#00ffff bold")]),
|
|
76
|
+
).ask()
|
|
77
|
+
|
|
78
|
+
if mode == "Exit" or mode is None:
|
|
79
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if mode == "Foundry Ops (Internal Side)":
|
|
83
|
+
internal_ops_menu()
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Check for alpha package expiration
|
|
87
|
+
alpha_mgr = get_expiration_manager()
|
|
88
|
+
if alpha_mgr.is_alpha():
|
|
89
|
+
alpha_mgr.show_status()
|
|
90
|
+
if alpha_mgr.is_near_expiration():
|
|
91
|
+
show_warning_message(alpha_mgr.alpha_info)
|
|
92
|
+
|
|
93
|
+
while True:
|
|
94
|
+
# Build menu choices based on auth status
|
|
95
|
+
base_choices = [
|
|
96
|
+
"Explore Templates",
|
|
97
|
+
"Generate New Project",
|
|
98
|
+
"Manage Stack Configuration",
|
|
99
|
+
"Run System Validation (Smoke Tests)",
|
|
100
|
+
"View Documentation",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Add auth option if not authenticated
|
|
104
|
+
if auth_info.get("tier") == "free":
|
|
105
|
+
base_choices.insert(0, "🔐 Login to Unlock PRO Features")
|
|
106
|
+
else:
|
|
107
|
+
base_choices.insert(0, f"👤 Account ({auth_info.get('tier', 'free').upper()})")
|
|
108
|
+
|
|
109
|
+
base_choices.append("Exit")
|
|
110
|
+
|
|
111
|
+
choice = questionary.select(
|
|
112
|
+
"What would you like to do?",
|
|
113
|
+
choices=base_choices,
|
|
114
|
+
).ask()
|
|
115
|
+
|
|
116
|
+
if choice == "🔐 Login to Unlock PRO Features":
|
|
117
|
+
try:
|
|
118
|
+
login_flow()
|
|
119
|
+
# Refresh auth info after login
|
|
120
|
+
auth_info = get_current_license_info()
|
|
121
|
+
console.print("\n[green]✓ Authentication successful![/green]\n")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"\n[red]✗ Authentication failed:[/red] {e}\n")
|
|
124
|
+
elif choice == f"👤 Account ({auth_info.get('tier', 'free').upper()})":
|
|
125
|
+
# Show account info
|
|
126
|
+
console.print(f"\n[bold]Account Information:[/bold]")
|
|
127
|
+
console.print(f"Email: {auth_info.get('email', 'Not available')}")
|
|
128
|
+
console.print(f"Tier: {auth_info.get('tier', 'free').upper()}")
|
|
129
|
+
console.print(f"Expires: {auth_info.get('expires', 'N/A')}")
|
|
130
|
+
console.print(f"User ID: {auth_info.get('user_id', 'N/A')}\n")
|
|
131
|
+
pause()
|
|
132
|
+
elif choice == "Explore Templates":
|
|
133
|
+
run_explore()
|
|
134
|
+
pause()
|
|
135
|
+
elif choice == "Generate New Project":
|
|
136
|
+
# Check expiration before generation
|
|
137
|
+
if not alpha_mgr.check_before_action("Generate New Project"):
|
|
138
|
+
continue
|
|
139
|
+
run_generator(interactive=True)
|
|
140
|
+
pause()
|
|
141
|
+
elif choice == "Manage Stack Configuration":
|
|
142
|
+
manage_config_menu()
|
|
143
|
+
elif choice == "Run System Validation (Smoke Tests)":
|
|
144
|
+
run_smoke_tests()
|
|
145
|
+
pause()
|
|
146
|
+
elif choice == "View Documentation":
|
|
147
|
+
run_view_docs(interactive=True)
|
|
148
|
+
pause()
|
|
149
|
+
else:
|
|
150
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
151
|
+
break
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import questionary
|
|
2
|
+
from foundry.constants import console
|
|
3
|
+
from foundry.actions.health import run_health_check
|
|
4
|
+
from foundry.actions.setup import run_setup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def manage_config_menu():
|
|
8
|
+
while True:
|
|
9
|
+
action = questionary.select(
|
|
10
|
+
"Configuration Management:",
|
|
11
|
+
choices=[
|
|
12
|
+
"View Validation Status",
|
|
13
|
+
"Run Template Setup (Full Suite)",
|
|
14
|
+
"Back",
|
|
15
|
+
],
|
|
16
|
+
).ask()
|
|
17
|
+
|
|
18
|
+
if action == "Back":
|
|
19
|
+
break
|
|
20
|
+
|
|
21
|
+
if action == "View Validation Status":
|
|
22
|
+
run_health_check()
|
|
23
|
+
pause()
|
|
24
|
+
|
|
25
|
+
if action == "Run Template Setup (Full Suite)":
|
|
26
|
+
run_setup()
|
|
27
|
+
pause()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pause():
|
|
31
|
+
input("\nPress Enter to return...")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def internal_ops_menu():
|
|
35
|
+
"""Menu for internal development and ops tasks."""
|
|
36
|
+
try:
|
|
37
|
+
from foundry.ops.manager import InternalManager
|
|
38
|
+
except ImportError:
|
|
39
|
+
console.print("[red]Ops manager not available in this build.[/]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Show auth status for internal devs too
|
|
43
|
+
from foundry.auth import get_current_license_info
|
|
44
|
+
auth_info = get_current_license_info()
|
|
45
|
+
if auth_info.get("tier") != "free":
|
|
46
|
+
console.print(f"[green]✓ Authenticated as {auth_info.get('tier', 'user').upper()}[/green]")
|
|
47
|
+
else:
|
|
48
|
+
console.print("[yellow]⚠️ Not authenticated - limited access[/yellow]")
|
|
49
|
+
|
|
50
|
+
manager = InternalManager()
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
choice = questionary.select(
|
|
54
|
+
"Foundry Ops - Internal Development:",
|
|
55
|
+
choices=[
|
|
56
|
+
"Verify All Builds (Docker)",
|
|
57
|
+
"Run Hygiene Checks (Repo Health)",
|
|
58
|
+
"Full Verification Suite",
|
|
59
|
+
"Clean Verification Reports",
|
|
60
|
+
"Back to Main",
|
|
61
|
+
],
|
|
62
|
+
style=questionary.Style([("highlighted", "fg:#ff00ff bold")]),
|
|
63
|
+
).ask()
|
|
64
|
+
|
|
65
|
+
if not choice or choice == "Back to Main":
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
if choice == "Verify All Builds (Docker)":
|
|
69
|
+
manager.verify_builds()
|
|
70
|
+
pause()
|
|
71
|
+
elif choice == "Run Hygiene Checks (Repo Health)":
|
|
72
|
+
manager.run_verification_script(
|
|
73
|
+
"verify_hygiene", "Code Hygiene (File Lengths)"
|
|
74
|
+
)
|
|
75
|
+
pause()
|
|
76
|
+
elif choice == "Full Verification Suite":
|
|
77
|
+
manager.verify_full_suite()
|
|
78
|
+
pause()
|
|
79
|
+
elif choice == "Clean Verification Reports":
|
|
80
|
+
manager.clean_reports()
|
|
81
|
+
pause()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from foundry.constants import ALPHA_FEEDBACK_FORM
|
|
4
|
+
|
|
5
|
+
LOG_FILE = Path(".session.log")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def log_event(event_type: str, details: str = ""):
|
|
9
|
+
"""Log an event to the local session log."""
|
|
10
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
11
|
+
log_entry = f"[{timestamp}] {event_type.upper()}: {details}\n"
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
|
15
|
+
f.write(log_entry)
|
|
16
|
+
except Exception:
|
|
17
|
+
# Fallback to silent failure to not break the CLI experience
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_feedback_link() -> str:
|
|
22
|
+
"""Returns the feedback form link."""
|
|
23
|
+
return ALPHA_FEEDBACK_FORM
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from foundry.constants import TEMPLATE_REGISTRY, TEMPLATES_DIR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_valid_template(path: Path) -> bool:
|
|
9
|
+
"""Check if a local directory is a valid template."""
|
|
10
|
+
if not path.is_dir() or path.name.startswith("."):
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
# Check if it's in our registry
|
|
14
|
+
if path.name in TEMPLATE_REGISTRY:
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
# Check for legacy project markers (fallback)
|
|
18
|
+
markers = [
|
|
19
|
+
"Dockerfile",
|
|
20
|
+
"package.json",
|
|
21
|
+
"main.tf",
|
|
22
|
+
"pyproject.toml",
|
|
23
|
+
"Gemfile",
|
|
24
|
+
"build.gradle.kts",
|
|
25
|
+
"StackApp",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
return any((path / marker).exists() for marker in markers)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_internal_dev() -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Check if the CLI is running in the internal development mono-repo context.
|
|
34
|
+
Looks for markers like .github, ops/, and scripts/ at the root.
|
|
35
|
+
Can be explicitly disabled with FOUNDRY_INTERNAL_DEV=0
|
|
36
|
+
"""
|
|
37
|
+
if os.getenv("FOUNDRY_INTERNAL_DEV") == "0":
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
internal_markers = [".github", "ops", "scripts", "wiring"]
|
|
41
|
+
return all((TEMPLATES_DIR / marker).exists() for marker in internal_markers)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_templates() -> List[Path]:
|
|
45
|
+
"""
|
|
46
|
+
Get list of templates.
|
|
47
|
+
In development mode, returns local directories.
|
|
48
|
+
In PyPI mode, returns virtual paths based on registry.
|
|
49
|
+
"""
|
|
50
|
+
# 1. Try local scanning (Development/Monorepo mode)
|
|
51
|
+
if TEMPLATES_DIR.exists():
|
|
52
|
+
local_templates = [d for d in TEMPLATES_DIR.iterdir() if is_valid_template(d)]
|
|
53
|
+
if local_templates:
|
|
54
|
+
return sorted(local_templates, key=lambda x: x.name)
|
|
55
|
+
|
|
56
|
+
# 2. Fallback to Registry (PyPI mode)
|
|
57
|
+
# We return dummy Path objects where the name corresponds to the registry key
|
|
58
|
+
return [Path(name) for name in TEMPLATE_REGISTRY.keys()]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_template_info(template_name: str) -> Dict[str, Any]:
|
|
62
|
+
"""Get metadata for a specific template."""
|
|
63
|
+
return TEMPLATE_REGISTRY.get(
|
|
64
|
+
template_name,
|
|
65
|
+
{"name": template_name, "description": "Custom Template", "tier": "free"},
|
|
66
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sscli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Seed & Source CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer[all]",
|
|
13
|
+
"rich",
|
|
14
|
+
"questionary",
|
|
15
|
+
"requests",
|
|
16
|
+
"pathlib",
|
|
17
|
+
"python-dotenv"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
stack-cli = "foundry.manage:app"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["foundry"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-data]
|
|
27
|
+
"*" = ["*.md", "*.json"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.exclude-package-data]
|
|
30
|
+
"*" = ["venv*", ".env*", "*.log", ".pytest_cache*", "tests*"]
|
sscli-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sscli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Seed & Source CLI
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer[all]
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: questionary
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: pathlib
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
13
|
+
|
|
14
|
+
# Seed & Source CLI
|
|
15
|
+
Unified interface for Stack Foundry templates.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
foundry/__init__.py
|
|
4
|
+
foundry/alpha_expiration.py
|
|
5
|
+
foundry/animated_experience.py
|
|
6
|
+
foundry/auth.py
|
|
7
|
+
foundry/cli.py
|
|
8
|
+
foundry/constants.py
|
|
9
|
+
foundry/interactive.py
|
|
10
|
+
foundry/interactive_utils.py
|
|
11
|
+
foundry/manage.py
|
|
12
|
+
foundry/telemetry.py
|
|
13
|
+
foundry/utils.py
|
|
14
|
+
sscli.egg-info/PKG-INFO
|
|
15
|
+
sscli.egg-info/SOURCES.txt
|
|
16
|
+
sscli.egg-info/dependency_links.txt
|
|
17
|
+
sscli.egg-info/entry_points.txt
|
|
18
|
+
sscli.egg-info/requires.txt
|
|
19
|
+
sscli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
foundry
|