mint-sdk 1.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mint_sdk/__init__.py +131 -0
- mint_sdk/_discover.py +159 -0
- mint_sdk/_prompt.py +228 -0
- mint_sdk/_version.py +24 -0
- mint_sdk/ai_instructions.md.template +169 -0
- mint_sdk/app.py +264 -0
- mint_sdk/cli.py +596 -0
- mint_sdk/cli_commands/__init__.py +0 -0
- mint_sdk/cli_commands/_utils.py +22 -0
- mint_sdk/cli_commands/auth_cmd.py +112 -0
- mint_sdk/cli_commands/experiment_cmd.py +198 -0
- mint_sdk/cli_commands/project_cmd.py +95 -0
- mint_sdk/cli_commands/status_cmd.py +68 -0
- mint_sdk/client/__init__.py +35 -0
- mint_sdk/client/_config.py +118 -0
- mint_sdk/client/_exceptions.py +130 -0
- mint_sdk/client/_http.py +119 -0
- mint_sdk/client/_types.py +90 -0
- mint_sdk/client/client.py +125 -0
- mint_sdk/client/resources/__init__.py +0 -0
- mint_sdk/client/resources/auth.py +75 -0
- mint_sdk/client/resources/experiments.py +277 -0
- mint_sdk/client/resources/plugins.py +18 -0
- mint_sdk/client/resources/projects.py +82 -0
- mint_sdk/context.py +103 -0
- mint_sdk/deps_command.py +271 -0
- mint_sdk/dev_command.py +461 -0
- mint_sdk/docs/__init__.py +1 -0
- mint_sdk/docs/bundled/frontend.json +6845 -0
- mint_sdk/docs/cache.py +126 -0
- mint_sdk/docs/formatter.py +709 -0
- mint_sdk/docs/frontend_extractor.py +79 -0
- mint_sdk/docs/python_extractor.py +442 -0
- mint_sdk/docs/search.py +137 -0
- mint_sdk/docs_command.py +293 -0
- mint_sdk/doctor_command.py +276 -0
- mint_sdk/exceptions.py +311 -0
- mint_sdk/export.py +348 -0
- mint_sdk/info_command.py +74 -0
- mint_sdk/init_command.py +601 -0
- mint_sdk/init_templates.py +968 -0
- mint_sdk/link_command.py +218 -0
- mint_sdk/local_database.py +163 -0
- mint_sdk/logging.py +49 -0
- mint_sdk/logs_command.py +137 -0
- mint_sdk/migrations/__init__.py +22 -0
- mint_sdk/migrations/base.py +54 -0
- mint_sdk/migrations/errors.py +54 -0
- mint_sdk/migrations/locking.py +64 -0
- mint_sdk/migrations/ops.py +302 -0
- mint_sdk/migrations/runner.py +313 -0
- mint_sdk/models.py +51 -0
- mint_sdk/plugin.py +610 -0
- mint_sdk/py.typed +0 -0
- mint_sdk/remote_context.py +323 -0
- mint_sdk/repositories.py +277 -0
- mint_sdk/testing/__init__.py +36 -0
- mint_sdk/testing/plugins.py +135 -0
- mint_sdk/testing/recording_context.py +185 -0
- mint_sdk/testing/subprocess.py +88 -0
- mint_sdk/update_command.py +260 -0
- mint_sdk-1.0.0a1.dist-info/METADATA +155 -0
- mint_sdk-1.0.0a1.dist-info/RECORD +65 -0
- mint_sdk-1.0.0a1.dist-info/WHEEL +4 -0
- mint_sdk-1.0.0a1.dist-info/entry_points.txt +2 -0
mint_sdk/__init__.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MINT Plugin SDK
|
|
3
|
+
|
|
4
|
+
SDK for building analysis plugins that integrate with the MINT platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from mint_sdk.context import PlatformContext
|
|
8
|
+
|
|
9
|
+
# Exceptions
|
|
10
|
+
from mint_sdk.exceptions import (
|
|
11
|
+
ConfigurationException,
|
|
12
|
+
ConflictException,
|
|
13
|
+
NotFoundException,
|
|
14
|
+
PermissionException,
|
|
15
|
+
PluginException,
|
|
16
|
+
PluginLifecycleException,
|
|
17
|
+
RepositoryException,
|
|
18
|
+
ValidationException,
|
|
19
|
+
)
|
|
20
|
+
from mint_sdk.export import auto_json_to_csv, auto_json_to_summary, auto_json_to_tree
|
|
21
|
+
|
|
22
|
+
# Local database (optional dependency)
|
|
23
|
+
from mint_sdk.local_database import LocalDatabase, LocalDatabaseConfig
|
|
24
|
+
from mint_sdk.logging import get_plugin_logger
|
|
25
|
+
|
|
26
|
+
# Migration framework (requires sqlalchemy — optional dependency)
|
|
27
|
+
try:
|
|
28
|
+
from mint_sdk.migrations import (
|
|
29
|
+
MigrationOps,
|
|
30
|
+
MigrationResult,
|
|
31
|
+
MigrationRunner,
|
|
32
|
+
PluginMigration,
|
|
33
|
+
)
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
from mint_sdk.models import PluginCapabilities, PluginMetadata, PluginType
|
|
37
|
+
from mint_sdk.plugin import (
|
|
38
|
+
AnalysisPlugin,
|
|
39
|
+
HealthStatus,
|
|
40
|
+
LifecycleHookResult,
|
|
41
|
+
PluginHealth,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Repository protocols and data models
|
|
45
|
+
from mint_sdk.repositories import (
|
|
46
|
+
DesignData,
|
|
47
|
+
# Data models
|
|
48
|
+
Experiment,
|
|
49
|
+
# Repository protocols
|
|
50
|
+
ExperimentRepository,
|
|
51
|
+
PlatformConfig,
|
|
52
|
+
PluginAnalysisResult,
|
|
53
|
+
PluginDataRepository,
|
|
54
|
+
PluginExperimentData, # backward compatibility alias for DesignData
|
|
55
|
+
PluginRoleRepository,
|
|
56
|
+
User,
|
|
57
|
+
UserPluginRole,
|
|
58
|
+
UserRepository,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# App factory and helpers
|
|
62
|
+
from mint_sdk.app import (
|
|
63
|
+
PluginDependency,
|
|
64
|
+
SPAStaticFiles,
|
|
65
|
+
create_standalone_app,
|
|
66
|
+
require_context,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# API Client
|
|
70
|
+
from mint_sdk.client import MINTClient
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
from mint_sdk._version import __version__
|
|
74
|
+
except ImportError:
|
|
75
|
+
__version__ = "0.0.0"
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
# API Client
|
|
79
|
+
"MINTClient",
|
|
80
|
+
# Core plugin classes
|
|
81
|
+
"AnalysisPlugin",
|
|
82
|
+
"PluginMetadata",
|
|
83
|
+
"PluginCapabilities",
|
|
84
|
+
"PluginType",
|
|
85
|
+
"PlatformContext",
|
|
86
|
+
# Lifecycle types
|
|
87
|
+
"HealthStatus",
|
|
88
|
+
"PluginHealth",
|
|
89
|
+
"LifecycleHookResult",
|
|
90
|
+
# Exceptions
|
|
91
|
+
"PluginException",
|
|
92
|
+
"ValidationException",
|
|
93
|
+
"PermissionException",
|
|
94
|
+
"ConfigurationException",
|
|
95
|
+
"RepositoryException",
|
|
96
|
+
"NotFoundException",
|
|
97
|
+
"ConflictException",
|
|
98
|
+
"PluginLifecycleException",
|
|
99
|
+
# Local database
|
|
100
|
+
"LocalDatabase",
|
|
101
|
+
"LocalDatabaseConfig",
|
|
102
|
+
# Data models
|
|
103
|
+
"Experiment",
|
|
104
|
+
"DesignData",
|
|
105
|
+
"PluginExperimentData", # backward compatibility alias
|
|
106
|
+
"PluginAnalysisResult",
|
|
107
|
+
"User",
|
|
108
|
+
"UserPluginRole",
|
|
109
|
+
# Repository protocols
|
|
110
|
+
"ExperimentRepository",
|
|
111
|
+
"PluginDataRepository",
|
|
112
|
+
"PluginRoleRepository",
|
|
113
|
+
"UserRepository",
|
|
114
|
+
"PlatformConfig",
|
|
115
|
+
# Export utilities
|
|
116
|
+
"auto_json_to_tree",
|
|
117
|
+
"auto_json_to_csv",
|
|
118
|
+
"auto_json_to_summary",
|
|
119
|
+
# Logging
|
|
120
|
+
"get_plugin_logger",
|
|
121
|
+
# Migration framework
|
|
122
|
+
"MigrationOps",
|
|
123
|
+
"MigrationResult",
|
|
124
|
+
"MigrationRunner",
|
|
125
|
+
"PluginMigration",
|
|
126
|
+
# App factory and helpers
|
|
127
|
+
"create_standalone_app",
|
|
128
|
+
"SPAStaticFiles",
|
|
129
|
+
"PluginDependency",
|
|
130
|
+
"require_context",
|
|
131
|
+
]
|
mint_sdk/_discover.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Plugin discovery from pyproject.toml without importing the plugin.
|
|
2
|
+
|
|
3
|
+
Parses entry points, module paths, and routes prefix by inspecting files
|
|
4
|
+
statically — no plugin code is executed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import tomllib
|
|
16
|
+
except ModuleNotFoundError: # Python < 3.11
|
|
17
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class DiscoveredPlugin:
|
|
22
|
+
"""Statically discovered plugin metadata."""
|
|
23
|
+
|
|
24
|
+
entry_point_name: str # "drp-analysis"
|
|
25
|
+
module_path: str # "mld_plugin_drp.plugin"
|
|
26
|
+
class_name: str # "DRPAnalysisPlugin"
|
|
27
|
+
factory_target: str # "mld_plugin_drp.plugin:create_standalone_app"
|
|
28
|
+
routes_prefix: str # "/drp" (from source) or "/drp-analysis" (fallback)
|
|
29
|
+
project_name: str # "mld-plugin-drp"
|
|
30
|
+
project_version: str | None # "0.2.0" or None if dynamic
|
|
31
|
+
project_description: str # from [project].description
|
|
32
|
+
has_frontend: bool # frontend/package.json exists
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_full_pyproject(project_dir: Path) -> dict:
|
|
36
|
+
"""Read and return the full pyproject.toml as a dict."""
|
|
37
|
+
path = project_dir / "pyproject.toml"
|
|
38
|
+
if not path.exists():
|
|
39
|
+
print(f"Error: {path} not found.", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
with open(path, "rb") as f:
|
|
42
|
+
return tomllib.load(f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_entry_point(data: dict) -> tuple[str, str, str] | None:
|
|
46
|
+
"""Extract (name, module_path, class_name) from [project.entry-points."mld.plugins"].
|
|
47
|
+
|
|
48
|
+
Returns None if no mld.plugins entry point is found.
|
|
49
|
+
"""
|
|
50
|
+
entry_points = (
|
|
51
|
+
data.get("project", {}).get("entry-points", {}).get("mld.plugins", {})
|
|
52
|
+
)
|
|
53
|
+
if not entry_points:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Take the first entry point
|
|
57
|
+
name, value = next(iter(entry_points.items()))
|
|
58
|
+
# value is "mld_plugin_drp.plugin:DRPAnalysisPlugin"
|
|
59
|
+
if ":" not in value:
|
|
60
|
+
return None
|
|
61
|
+
module_path, class_name = value.rsplit(":", 1)
|
|
62
|
+
return name, module_path, class_name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_routes_prefix_from_source(
|
|
66
|
+
module_path: str, project_dir: Path
|
|
67
|
+
) -> str | None:
|
|
68
|
+
"""Parse PLUGIN_ROUTES_PREFIX from the plugin source file without importing it.
|
|
69
|
+
|
|
70
|
+
Looks for patterns like:
|
|
71
|
+
PLUGIN_ROUTES_PREFIX = "/drp"
|
|
72
|
+
PLUGIN_ROUTES_PREFIX = '/drp'
|
|
73
|
+
"""
|
|
74
|
+
# Convert module path to file path: mld_plugin_drp.plugin -> src/mld_plugin_drp/plugin.py
|
|
75
|
+
parts = module_path.split(".")
|
|
76
|
+
candidates = [
|
|
77
|
+
project_dir / "src" / Path(*parts).with_suffix(".py"),
|
|
78
|
+
project_dir / Path(*parts).with_suffix(".py"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
for source_path in candidates:
|
|
82
|
+
if not source_path.exists():
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
content = source_path.read_text()
|
|
86
|
+
except OSError:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
match = re.search(
|
|
90
|
+
r'PLUGIN_ROUTES_PREFIX\s*=\s*["\'](/[^"\']+)["\']', content
|
|
91
|
+
)
|
|
92
|
+
if match:
|
|
93
|
+
return match.group(1)
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def discover_plugin(project_dir: Path) -> DiscoveredPlugin:
|
|
99
|
+
"""Discover plugin metadata from pyproject.toml and source files.
|
|
100
|
+
|
|
101
|
+
Raises SystemExit if the project directory lacks required configuration.
|
|
102
|
+
"""
|
|
103
|
+
data = _read_full_pyproject(project_dir)
|
|
104
|
+
|
|
105
|
+
project = data.get("project")
|
|
106
|
+
if not project:
|
|
107
|
+
print("Error: pyproject.toml has no [project] table.", file=sys.stderr)
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
entry_point = _parse_entry_point(data)
|
|
111
|
+
if not entry_point:
|
|
112
|
+
print(
|
|
113
|
+
"Error: No [project.entry-points.\"mld.plugins\"] found in pyproject.toml.",
|
|
114
|
+
file=sys.stderr,
|
|
115
|
+
)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
ep_name, module_path, class_name = entry_point
|
|
119
|
+
|
|
120
|
+
# Routes prefix: parse from source, fallback to /{entry_point_name}
|
|
121
|
+
routes_prefix = _parse_routes_prefix_from_source(module_path, project_dir)
|
|
122
|
+
if routes_prefix is None:
|
|
123
|
+
routes_prefix = f"/{ep_name}"
|
|
124
|
+
|
|
125
|
+
# Factory target for uvicorn --factory
|
|
126
|
+
factory_target = f"{module_path}:create_standalone_app"
|
|
127
|
+
|
|
128
|
+
# Version: static or None if dynamic
|
|
129
|
+
version = project.get("version")
|
|
130
|
+
|
|
131
|
+
has_frontend = (project_dir / "frontend" / "package.json").exists()
|
|
132
|
+
|
|
133
|
+
return DiscoveredPlugin(
|
|
134
|
+
entry_point_name=ep_name,
|
|
135
|
+
module_path=module_path,
|
|
136
|
+
class_name=class_name,
|
|
137
|
+
factory_target=factory_target,
|
|
138
|
+
routes_prefix=routes_prefix,
|
|
139
|
+
project_name=project["name"],
|
|
140
|
+
project_version=version,
|
|
141
|
+
project_description=project.get("description", ""),
|
|
142
|
+
has_frontend=has_frontend,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_sdk_dependency_spec(project_dir: Path) -> str | None:
|
|
147
|
+
"""Extract the mint-sdk version specifier from pyproject.toml dependencies.
|
|
148
|
+
|
|
149
|
+
Returns e.g. ">=0.9.4" or None if mint-sdk is not in dependencies.
|
|
150
|
+
"""
|
|
151
|
+
data = _read_full_pyproject(project_dir)
|
|
152
|
+
deps = data.get("project", {}).get("dependencies", [])
|
|
153
|
+
for dep in deps:
|
|
154
|
+
# Match "mint-sdk>=0.9.4", "mint-sdk>=0.9.4,<1.0", "mint-sdk"
|
|
155
|
+
match = re.match(r"mint[-_]sdk\s*(.*)", dep, re.IGNORECASE)
|
|
156
|
+
if match:
|
|
157
|
+
spec = match.group(1).strip()
|
|
158
|
+
return spec if spec else "*"
|
|
159
|
+
return None
|
mint_sdk/_prompt.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Interactive terminal prompts with arrow-key selection (gh-style).
|
|
2
|
+
|
|
3
|
+
Zero external dependencies — uses ``tty``/``termios`` for raw input on
|
|
4
|
+
Unix systems. Falls back to plain ``input()`` when stdin is not a TTY
|
|
5
|
+
or on unsupported platforms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# ANSI helpers
|
|
13
|
+
_BOLD = "\033[1m"
|
|
14
|
+
_CYAN = "\033[36m"
|
|
15
|
+
_GREEN = "\033[32m"
|
|
16
|
+
_DIM = "\033[2m"
|
|
17
|
+
_RESET = "\033[0m"
|
|
18
|
+
_CLEAR_LINE = "\033[2K"
|
|
19
|
+
_HIDE_CURSOR = "\033[?25l"
|
|
20
|
+
_SHOW_CURSOR = "\033[?25h"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_interactive() -> bool:
|
|
24
|
+
"""Return True when stdin/stdout are both TTYs."""
|
|
25
|
+
try:
|
|
26
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
27
|
+
except Exception:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_key() -> str:
|
|
32
|
+
"""Read a single keypress (or escape sequence) in raw mode."""
|
|
33
|
+
import termios
|
|
34
|
+
import tty
|
|
35
|
+
|
|
36
|
+
fd = sys.stdin.fileno()
|
|
37
|
+
old = termios.tcgetattr(fd)
|
|
38
|
+
try:
|
|
39
|
+
tty.setraw(fd)
|
|
40
|
+
ch = sys.stdin.read(1)
|
|
41
|
+
if ch == "\x1b":
|
|
42
|
+
ch2 = sys.stdin.read(1)
|
|
43
|
+
if ch2 == "[":
|
|
44
|
+
ch3 = sys.stdin.read(1)
|
|
45
|
+
return f"\x1b[{ch3}"
|
|
46
|
+
return ch + ch2
|
|
47
|
+
return ch
|
|
48
|
+
finally:
|
|
49
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def prompt_text(label: str, default: str = "") -> str:
|
|
53
|
+
"""Prompt for free-form text input with a default value.
|
|
54
|
+
|
|
55
|
+
Example output::
|
|
56
|
+
|
|
57
|
+
? Plugin name (My Plugin):
|
|
58
|
+
"""
|
|
59
|
+
if not _is_interactive():
|
|
60
|
+
return default
|
|
61
|
+
|
|
62
|
+
suffix = f" ({default})" if default else ""
|
|
63
|
+
try:
|
|
64
|
+
answer = input(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}{_DIM}{suffix}{_RESET}: ").strip()
|
|
65
|
+
except (EOFError, KeyboardInterrupt):
|
|
66
|
+
print()
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
return answer or default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def prompt_select(label: str, choices: list[str], default: int = 0) -> int:
|
|
72
|
+
"""Arrow-key selection from a list of choices. Returns the chosen index.
|
|
73
|
+
|
|
74
|
+
Example output::
|
|
75
|
+
|
|
76
|
+
? Plugin type:
|
|
77
|
+
> Analysis
|
|
78
|
+
Experiment Design
|
|
79
|
+
|
|
80
|
+
Falls back to numbered input when not interactive.
|
|
81
|
+
"""
|
|
82
|
+
if not _is_interactive():
|
|
83
|
+
return default
|
|
84
|
+
|
|
85
|
+
cursor = default
|
|
86
|
+
|
|
87
|
+
def _render(first: bool = False) -> None:
|
|
88
|
+
if not first:
|
|
89
|
+
# Move cursor up to overwrite previous render
|
|
90
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
91
|
+
for i, choice in enumerate(choices):
|
|
92
|
+
sys.stdout.write(_CLEAR_LINE)
|
|
93
|
+
if i == cursor:
|
|
94
|
+
sys.stdout.write(f" {_CYAN}> {choice}{_RESET}\n")
|
|
95
|
+
else:
|
|
96
|
+
sys.stdout.write(f" {choice}\n")
|
|
97
|
+
sys.stdout.flush()
|
|
98
|
+
|
|
99
|
+
sys.stdout.write(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}\n")
|
|
100
|
+
sys.stdout.write(_HIDE_CURSOR)
|
|
101
|
+
sys.stdout.flush()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
_render(first=True)
|
|
105
|
+
while True:
|
|
106
|
+
key = _read_key()
|
|
107
|
+
if key == "\x1b[A": # Up
|
|
108
|
+
cursor = (cursor - 1) % len(choices)
|
|
109
|
+
_render()
|
|
110
|
+
elif key == "\x1b[B": # Down
|
|
111
|
+
cursor = (cursor + 1) % len(choices)
|
|
112
|
+
_render()
|
|
113
|
+
elif key in ("\r", "\n"): # Enter
|
|
114
|
+
break
|
|
115
|
+
elif key == "\x03": # Ctrl+C
|
|
116
|
+
raise KeyboardInterrupt
|
|
117
|
+
except (KeyboardInterrupt, EOFError):
|
|
118
|
+
sys.stdout.write(_SHOW_CURSOR)
|
|
119
|
+
sys.stdout.flush()
|
|
120
|
+
print()
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
# Replace the choice list with the final answer
|
|
124
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
125
|
+
for _ in choices:
|
|
126
|
+
sys.stdout.write(f"{_CLEAR_LINE}\n")
|
|
127
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
128
|
+
sys.stdout.write(f" {_CYAN}{choices[cursor]}{_RESET}\n")
|
|
129
|
+
sys.stdout.write(_SHOW_CURSOR)
|
|
130
|
+
sys.stdout.flush()
|
|
131
|
+
|
|
132
|
+
return cursor
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def prompt_multiselect(
|
|
136
|
+
label: str,
|
|
137
|
+
choices: list[str],
|
|
138
|
+
defaults: list[int] | None = None,
|
|
139
|
+
) -> list[int]:
|
|
140
|
+
"""Arrow-key multi-select with space to toggle. Returns selected indices.
|
|
141
|
+
|
|
142
|
+
Example output::
|
|
143
|
+
|
|
144
|
+
? AI coding assistant (space = toggle, enter = confirm)
|
|
145
|
+
[x] Claude Code
|
|
146
|
+
[ ] Codex (OpenAI)
|
|
147
|
+
[x] Cursor
|
|
148
|
+
[ ] Windsurf
|
|
149
|
+
[ ] None
|
|
150
|
+
|
|
151
|
+
Falls back to returning *defaults* when not interactive.
|
|
152
|
+
"""
|
|
153
|
+
if not _is_interactive():
|
|
154
|
+
return list(defaults) if defaults else []
|
|
155
|
+
|
|
156
|
+
selected = set(defaults) if defaults else set()
|
|
157
|
+
cursor = 0
|
|
158
|
+
|
|
159
|
+
hint = f"{_DIM} (space = toggle, enter = confirm){_RESET}"
|
|
160
|
+
|
|
161
|
+
def _render(first: bool = False) -> None:
|
|
162
|
+
if not first:
|
|
163
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
164
|
+
for i, choice in enumerate(choices):
|
|
165
|
+
sys.stdout.write(_CLEAR_LINE)
|
|
166
|
+
check = "x" if i in selected else " "
|
|
167
|
+
if i == cursor:
|
|
168
|
+
sys.stdout.write(f" {_CYAN}>[{check}] {choice}{_RESET}\n")
|
|
169
|
+
else:
|
|
170
|
+
sys.stdout.write(f" [{check}] {choice}\n")
|
|
171
|
+
sys.stdout.flush()
|
|
172
|
+
|
|
173
|
+
sys.stdout.write(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}{hint}\n")
|
|
174
|
+
sys.stdout.write(_HIDE_CURSOR)
|
|
175
|
+
sys.stdout.flush()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
_render(first=True)
|
|
179
|
+
while True:
|
|
180
|
+
key = _read_key()
|
|
181
|
+
if key == "\x1b[A": # Up
|
|
182
|
+
cursor = (cursor - 1) % len(choices)
|
|
183
|
+
_render()
|
|
184
|
+
elif key == "\x1b[B": # Down
|
|
185
|
+
cursor = (cursor + 1) % len(choices)
|
|
186
|
+
_render()
|
|
187
|
+
elif key == " ": # Space — toggle
|
|
188
|
+
if cursor in selected:
|
|
189
|
+
selected.discard(cursor)
|
|
190
|
+
else:
|
|
191
|
+
selected.add(cursor)
|
|
192
|
+
_render()
|
|
193
|
+
elif key in ("\r", "\n"): # Enter — confirm
|
|
194
|
+
break
|
|
195
|
+
elif key == "\x03": # Ctrl+C
|
|
196
|
+
raise KeyboardInterrupt
|
|
197
|
+
except (KeyboardInterrupt, EOFError):
|
|
198
|
+
sys.stdout.write(_SHOW_CURSOR)
|
|
199
|
+
sys.stdout.flush()
|
|
200
|
+
print()
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
# Collapse to summary line
|
|
204
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
205
|
+
for _ in choices:
|
|
206
|
+
sys.stdout.write(f"{_CLEAR_LINE}\n")
|
|
207
|
+
sys.stdout.write(f"\033[{len(choices)}A")
|
|
208
|
+
picked = [choices[i] for i in sorted(selected)]
|
|
209
|
+
summary = ", ".join(picked) if picked else "None"
|
|
210
|
+
sys.stdout.write(f" {_CYAN}{summary}{_RESET}\n")
|
|
211
|
+
sys.stdout.write(_SHOW_CURSOR)
|
|
212
|
+
sys.stdout.flush()
|
|
213
|
+
|
|
214
|
+
return sorted(selected)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def prompt_confirm(label: str, default: bool = True) -> bool:
|
|
218
|
+
"""Yes/No selection with arrow keys.
|
|
219
|
+
|
|
220
|
+
Example output::
|
|
221
|
+
|
|
222
|
+
? Include frontend?
|
|
223
|
+
> Yes
|
|
224
|
+
No
|
|
225
|
+
"""
|
|
226
|
+
choices = ["Yes", "No"]
|
|
227
|
+
idx = prompt_select(label, choices, default=0 if default else 1)
|
|
228
|
+
return idx == 0
|
mint_sdk/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.0.0a1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0, 'a1')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|