splent-cli 1.2.7__tar.gz → 1.2.8__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.
- {splent_cli-1.2.7/src/splent_cli.egg-info → splent_cli-1.2.8}/PKG-INFO +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/pyproject.toml +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_orphans.py +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_deps.py +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_features.py +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/coverage.py +5 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_dump.py +21 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_migrate.py +5 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_restore.py +3 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_seed.py +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_set.py +15 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/export_puml.py +65 -44
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_add.py +5 -4
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_attach.py +105 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_clone.py +39 -12
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_create.py +19 -3
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_delete.py +2 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_discard.py +2 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_edit.py +44 -4
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_fork.py +3 -1
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_pip_install.py +96 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_release.py +30 -6
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature_compile.py +7 -11
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/locust.py +10 -4
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_build.py +54 -15
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_clean.py +4 -0
- splent_cli-1.2.8/src/splent_cli/commands/product/product_derive.py +269 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_release.py +45 -15
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_select.py +3 -3
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync.py +5 -3
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_up.py +33 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/selenium.py +5 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fetch.py +1 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_sync.py +5 -3
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/version.py +3 -3
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/services/context.py +11 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/services/release.py +74 -25
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/manifest.py +5 -2
- {splent_cli-1.2.7 → splent_cli-1.2.8/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- splent_cli-1.2.7/src/splent_cli/commands/feature/feature_attach.py +0 -173
- splent_cli-1.2.7/src/splent_cli/commands/product/product_derive.py +0 -134
- {splent_cli-1.2.7 → splent_cli-1.2.8}/LICENSE +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/README.md +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/setup.cfg +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/clear_cache.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/clear_log.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/clear_uploads.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/command_create.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_diff.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/linter.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_status.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/route_list.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/tokens.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_check.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_configs.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_deps.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_features.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fix.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_info.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_missing.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_valid.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.2.7 → splent_cli-1.2.8}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
4
4
|
Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
|
|
@@ -108,7 +108,7 @@ def _scan_feature_imports(
|
|
|
108
108
|
try:
|
|
109
109
|
with open(filepath, "r", encoding="utf-8") as fh:
|
|
110
110
|
content = fh.read()
|
|
111
|
-
except
|
|
111
|
+
except (OSError, PermissionError):
|
|
112
112
|
continue
|
|
113
113
|
|
|
114
114
|
# Find imports: from splent_io.splent_feature_X... or import splent_io.splent_feature_X
|
|
@@ -139,7 +139,7 @@ def check_features():
|
|
|
139
139
|
_warn("Uncommitted changes")
|
|
140
140
|
else:
|
|
141
141
|
_ok("Git clean")
|
|
142
|
-
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
142
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
143
143
|
_warn("Could not check git status")
|
|
144
144
|
|
|
145
145
|
click.echo()
|
|
@@ -36,5 +36,8 @@ def coverage(module_name, html):
|
|
|
36
36
|
|
|
37
37
|
try:
|
|
38
38
|
subprocess.run(coverage_cmd, check=True)
|
|
39
|
-
except subprocess.CalledProcessError
|
|
40
|
-
click.echo(
|
|
39
|
+
except subprocess.CalledProcessError:
|
|
40
|
+
click.echo(
|
|
41
|
+
click.style("❌ Coverage run failed (tests may be failing).", fg="red")
|
|
42
|
+
)
|
|
43
|
+
raise SystemExit(1)
|
|
@@ -18,6 +18,20 @@ def db_dump(filename):
|
|
|
18
18
|
mariadb_password = os.getenv("MARIADB_PASSWORD")
|
|
19
19
|
mariadb_database = os.getenv("MARIADB_DATABASE")
|
|
20
20
|
|
|
21
|
+
missing = [
|
|
22
|
+
k
|
|
23
|
+
for k, v in {
|
|
24
|
+
"MARIADB_HOSTNAME": mariadb_hostname,
|
|
25
|
+
"MARIADB_USER": mariadb_user,
|
|
26
|
+
"MARIADB_PASSWORD": mariadb_password,
|
|
27
|
+
"MARIADB_DATABASE": mariadb_database,
|
|
28
|
+
}.items()
|
|
29
|
+
if not v
|
|
30
|
+
]
|
|
31
|
+
if missing:
|
|
32
|
+
click.secho(f"❌ Missing env vars: {', '.join(missing)}", fg="red")
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
|
|
21
35
|
# Generate default filename if not provided
|
|
22
36
|
if not filename:
|
|
23
37
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -28,23 +42,29 @@ def db_dump(filename):
|
|
|
28
42
|
filename += ".sql"
|
|
29
43
|
|
|
30
44
|
try:
|
|
45
|
+
env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
|
|
31
46
|
with open(filename, "wb") as out:
|
|
32
47
|
subprocess.run(
|
|
33
48
|
[
|
|
34
49
|
"mysqldump",
|
|
35
50
|
f"-h{mariadb_hostname}",
|
|
36
51
|
f"-u{mariadb_user}",
|
|
37
|
-
f"-p{mariadb_password}",
|
|
38
52
|
mariadb_database,
|
|
39
53
|
],
|
|
40
54
|
stdout=out,
|
|
41
55
|
check=True,
|
|
56
|
+
env=env,
|
|
42
57
|
)
|
|
43
58
|
click.echo(
|
|
44
59
|
click.style(f"Database dump created successfully: {filename}", fg="green")
|
|
45
60
|
)
|
|
46
61
|
except subprocess.CalledProcessError as e:
|
|
47
62
|
click.echo(click.style(f"Error creating database dump: {e}", fg="red"))
|
|
63
|
+
if os.path.exists(filename):
|
|
64
|
+
os.remove(filename)
|
|
65
|
+
click.echo(
|
|
66
|
+
click.style(f"Partial file removed: {filename}", fg="yellow")
|
|
67
|
+
)
|
|
48
68
|
|
|
49
69
|
|
|
50
70
|
cli_command = db_dump
|
|
@@ -109,8 +109,11 @@ def db_migrate(feature):
|
|
|
109
109
|
|
|
110
110
|
try:
|
|
111
111
|
alembic_migrate(directory=mdir, message=feat)
|
|
112
|
-
except Exception:
|
|
113
|
-
|
|
112
|
+
except Exception as e:
|
|
113
|
+
if os.getenv("SPLENT_DEBUG"):
|
|
114
|
+
click.secho(
|
|
115
|
+
f" ⚠️ {feat}: migration generation skipped ({e})", fg="yellow"
|
|
116
|
+
)
|
|
114
117
|
finally:
|
|
115
118
|
alembic_logger.setLevel(prev_level)
|
|
116
119
|
|
|
@@ -57,11 +57,13 @@ def db_restore(filename, yes):
|
|
|
57
57
|
raise SystemExit(0)
|
|
58
58
|
|
|
59
59
|
try:
|
|
60
|
+
env = {**os.environ, "MYSQL_PWD": password or ""}
|
|
60
61
|
with open(filename, "rb") as sql_file:
|
|
61
62
|
subprocess.run(
|
|
62
|
-
["mysql", f"-h{host}", f"-u{user}",
|
|
63
|
+
["mysql", f"-h{host}", f"-u{user}", database],
|
|
63
64
|
stdin=sql_file,
|
|
64
65
|
check=True,
|
|
66
|
+
env=env,
|
|
65
67
|
)
|
|
66
68
|
click.secho(f"✅ Database restored from: {filename}", fg="green")
|
|
67
69
|
except subprocess.CalledProcessError as e:
|
|
@@ -26,7 +26,7 @@ def _resolve_feature_order(features_raw: list[str]) -> list[str]:
|
|
|
26
26
|
uvl_file = uvl_cfg.get("file")
|
|
27
27
|
if uvl_file:
|
|
28
28
|
uvl_path = os.path.join(product_dir, "uvl", uvl_file)
|
|
29
|
-
except
|
|
29
|
+
except (OSError, KeyError, AttributeError):
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
return FeatureLoadOrderResolver().resolve(features_raw, uvl_path)
|
|
@@ -24,6 +24,14 @@ def load_env():
|
|
|
24
24
|
return data
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _validate_env_value(value: str, label: str) -> str:
|
|
28
|
+
"""Strip whitespace and reject values containing newlines."""
|
|
29
|
+
value = value.strip()
|
|
30
|
+
if "\n" in value or "\r" in value:
|
|
31
|
+
raise click.ClickException(f"{label} must not contain newlines.")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
27
35
|
def write_env(env: dict):
|
|
28
36
|
"""Write dict back to .env."""
|
|
29
37
|
lines = [f"{k}={v}" for k, v in env.items()]
|
|
@@ -62,6 +70,9 @@ def set_github_interactive():
|
|
|
62
70
|
user = click.prompt("GitHub username", type=str)
|
|
63
71
|
token = click.prompt("GitHub personal access token", hide_input=True)
|
|
64
72
|
|
|
73
|
+
user = _validate_env_value(user, "GitHub username")
|
|
74
|
+
token = _validate_env_value(token, "GitHub token")
|
|
75
|
+
|
|
65
76
|
set_var("GITHUB_USER", user)
|
|
66
77
|
set_var("GITHUB_TOKEN", token)
|
|
67
78
|
|
|
@@ -73,6 +84,8 @@ def set_pypi_interactive():
|
|
|
73
84
|
username = "__token__"
|
|
74
85
|
token = click.prompt("PyPI token", hide_input=True)
|
|
75
86
|
|
|
87
|
+
token = _validate_env_value(token, "PyPI token")
|
|
88
|
+
|
|
76
89
|
set_var("PYPI_USERNAME", username)
|
|
77
90
|
set_var("PYPI_TOKEN", token)
|
|
78
91
|
|
|
@@ -86,9 +99,9 @@ def set_developer_interactive():
|
|
|
86
99
|
answer = click.prompt("(y/n)", type=click.Choice(["y", "n"], case_sensitive=False))
|
|
87
100
|
enabled = "true" if answer == "y" else "false"
|
|
88
101
|
|
|
89
|
-
set_var("
|
|
102
|
+
set_var("SPLENT_USE_SSH", enabled)
|
|
90
103
|
|
|
91
|
-
click.secho(f"✔
|
|
104
|
+
click.secho(f"✔ SPLENT_USE_SSH set to {enabled}", fg="green")
|
|
92
105
|
remind_source()
|
|
93
106
|
|
|
94
107
|
|
|
@@ -659,50 +659,66 @@ def _render_exports(
|
|
|
659
659
|
|
|
660
660
|
svg_path = f"{base}.svg"
|
|
661
661
|
click.echo("🔧 Rendering diagram...")
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
click.secho(
|
|
675
|
-
|
|
676
|
-
click.secho(
|
|
677
|
-
"❌ rsvg-convert not found. Rebuild: make setup-rebuild", fg="red"
|
|
678
|
-
)
|
|
662
|
+
try:
|
|
663
|
+
subprocess.run(
|
|
664
|
+
[plantuml_bin, "-tsvg", puml_path],
|
|
665
|
+
check=True,
|
|
666
|
+
capture_output=True,
|
|
667
|
+
)
|
|
668
|
+
except subprocess.CalledProcessError as e:
|
|
669
|
+
click.secho("❌ PlantUML failed to render diagram.", fg="red")
|
|
670
|
+
if os.getenv("SPLENT_DEBUG"):
|
|
671
|
+
stderr = e.stderr
|
|
672
|
+
if isinstance(stderr, bytes):
|
|
673
|
+
stderr = stderr.decode(errors="replace")
|
|
674
|
+
click.secho(stderr[:500], fg="bright_black")
|
|
675
|
+
return
|
|
679
676
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
"
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
)
|
|
698
|
-
click.secho(f"✅ PNG exported: {base}.png", fg="green")
|
|
699
|
-
except FileNotFoundError:
|
|
700
|
-
click.secho(
|
|
701
|
-
"❌ rsvg-convert not found. Rebuild: make setup-rebuild", fg="red"
|
|
702
|
-
)
|
|
677
|
+
try:
|
|
678
|
+
if export_svg:
|
|
679
|
+
click.secho(f"✅ SVG exported: {svg_path}", fg="green")
|
|
680
|
+
|
|
681
|
+
if export_pdf:
|
|
682
|
+
click.echo("📄 Converting to PDF...")
|
|
683
|
+
try:
|
|
684
|
+
subprocess.run(
|
|
685
|
+
["rsvg-convert", "-f", "pdf", "-o", f"{base}.pdf", svg_path],
|
|
686
|
+
check=True,
|
|
687
|
+
)
|
|
688
|
+
click.secho(f"✅ PDF exported: {base}.pdf", fg="green")
|
|
689
|
+
except FileNotFoundError:
|
|
690
|
+
click.secho(
|
|
691
|
+
"❌ rsvg-convert not found. Rebuild: make setup-rebuild",
|
|
692
|
+
fg="red",
|
|
693
|
+
)
|
|
703
694
|
|
|
704
|
-
|
|
705
|
-
|
|
695
|
+
if export_png:
|
|
696
|
+
click.echo("🖼️ Converting to PNG...")
|
|
697
|
+
try:
|
|
698
|
+
subprocess.run(
|
|
699
|
+
[
|
|
700
|
+
"rsvg-convert",
|
|
701
|
+
"-f",
|
|
702
|
+
"png",
|
|
703
|
+
"--dpi-x",
|
|
704
|
+
"150",
|
|
705
|
+
"--dpi-y",
|
|
706
|
+
"150",
|
|
707
|
+
"-o",
|
|
708
|
+
f"{base}.png",
|
|
709
|
+
svg_path,
|
|
710
|
+
],
|
|
711
|
+
check=True,
|
|
712
|
+
)
|
|
713
|
+
click.secho(f"✅ PNG exported: {base}.png", fg="green")
|
|
714
|
+
except FileNotFoundError:
|
|
715
|
+
click.secho(
|
|
716
|
+
"❌ rsvg-convert not found. Rebuild: make setup-rebuild",
|
|
717
|
+
fg="red",
|
|
718
|
+
)
|
|
719
|
+
finally:
|
|
720
|
+
if not export_svg and os.path.exists(svg_path):
|
|
721
|
+
os.remove(svg_path)
|
|
706
722
|
|
|
707
723
|
|
|
708
724
|
# ---------------------------------------------------------------------------
|
|
@@ -811,8 +827,13 @@ def export_puml(
|
|
|
811
827
|
if mode_classes:
|
|
812
828
|
all_models: dict[str, list[dict]] = {}
|
|
813
829
|
for package, fpath in feature_paths.items():
|
|
814
|
-
|
|
815
|
-
|
|
830
|
+
src_root = os.path.join(fpath, "src")
|
|
831
|
+
if not os.path.isdir(src_root):
|
|
832
|
+
continue
|
|
833
|
+
for org_dir in os.listdir(src_root):
|
|
834
|
+
models_path = os.path.join(
|
|
835
|
+
src_root, org_dir, package, "models.py"
|
|
836
|
+
)
|
|
816
837
|
if os.path.isfile(models_path):
|
|
817
838
|
parsed = _parse_models(models_path)
|
|
818
839
|
if parsed:
|
|
@@ -74,11 +74,12 @@ def feature_add(full_name):
|
|
|
74
74
|
os.makedirs(product_features_dir, exist_ok=True)
|
|
75
75
|
|
|
76
76
|
link_path = os.path.join(product_features_dir, feature_name)
|
|
77
|
-
if os.path.islink(link_path) or os.path.exists(link_path):
|
|
78
|
-
os.unlink(link_path)
|
|
79
|
-
|
|
80
77
|
rel_target = os.path.relpath(feature_dir, product_features_dir)
|
|
81
|
-
|
|
78
|
+
try:
|
|
79
|
+
os.symlink(rel_target, link_path)
|
|
80
|
+
except FileExistsError:
|
|
81
|
+
os.unlink(link_path)
|
|
82
|
+
os.symlink(rel_target, link_path)
|
|
82
83
|
click.echo(f"🔗 Linked {link_path} → {rel_target}")
|
|
83
84
|
|
|
84
85
|
# --------------------------
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tomllib
|
|
3
|
+
import tomli_w
|
|
4
|
+
import click
|
|
5
|
+
from splent_cli.services import context, compose
|
|
6
|
+
from splent_cli.utils.manifest import feature_key, set_feature_state
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(
|
|
10
|
+
"feature:attach",
|
|
11
|
+
short_help="Attach a cached feature version to the current product.",
|
|
12
|
+
)
|
|
13
|
+
@click.argument("feature_identifier", required=True)
|
|
14
|
+
@click.argument("version", required=True)
|
|
15
|
+
def feature_attach(feature_identifier, version):
|
|
16
|
+
"""
|
|
17
|
+
Attach a cached feature version to the current product.
|
|
18
|
+
|
|
19
|
+
- Requires the feature to already be in the local cache.
|
|
20
|
+
If not, run: splent feature:clone <namespace>/<feature>@<version>
|
|
21
|
+
- Updates pyproject.toml referencing feature@version.
|
|
22
|
+
- Creates/updates the versioned symlink in features/<namespace>/.
|
|
23
|
+
- Updates the manifest state to 'declared'.
|
|
24
|
+
"""
|
|
25
|
+
product = context.require_app()
|
|
26
|
+
ws = context.workspace()
|
|
27
|
+
|
|
28
|
+
# --- Parse feature identifier -------------------------------------------
|
|
29
|
+
namespace, namespace_github, namespace_fs, feature_name = (
|
|
30
|
+
compose.parse_feature_identifier(feature_identifier)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
cache_base = str(ws / ".splent_cache" / "features" / namespace_fs)
|
|
34
|
+
product_path = str(ws / product)
|
|
35
|
+
pyproject_path = os.path.join(product_path, "pyproject.toml")
|
|
36
|
+
|
|
37
|
+
if not os.path.exists(pyproject_path):
|
|
38
|
+
click.echo("❌ pyproject.toml not found in product.")
|
|
39
|
+
raise SystemExit(1)
|
|
40
|
+
|
|
41
|
+
# --- 1️⃣ Verify feature exists in cache ---------------------------------
|
|
42
|
+
versioned_dir = os.path.join(cache_base, f"{feature_name}@{version}")
|
|
43
|
+
|
|
44
|
+
if not os.path.exists(versioned_dir):
|
|
45
|
+
click.echo(
|
|
46
|
+
f"❌ Feature '{namespace}/{feature_name}@{version}' not found in cache.\n"
|
|
47
|
+
f" Run first: splent feature:clone {namespace}/{feature_name}@{version}"
|
|
48
|
+
)
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
|
|
51
|
+
click.echo(f"✅ Cache found → {versioned_dir}")
|
|
52
|
+
|
|
53
|
+
# --- 2️⃣ Update pyproject.toml ------------------------------------------
|
|
54
|
+
full_name = f"{namespace}/{feature_name}@{version}"
|
|
55
|
+
bare_name = f"{namespace}/{feature_name}"
|
|
56
|
+
|
|
57
|
+
with open(pyproject_path, "rb") as f:
|
|
58
|
+
data = tomllib.load(f)
|
|
59
|
+
|
|
60
|
+
from splent_cli.utils.feature_utils import (
|
|
61
|
+
read_features_from_data,
|
|
62
|
+
write_features_to_data,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
features = read_features_from_data(data)
|
|
66
|
+
|
|
67
|
+
if full_name in features:
|
|
68
|
+
click.echo(f"ℹ️ Feature '{full_name}' already present in pyproject.toml.")
|
|
69
|
+
else:
|
|
70
|
+
# Replace bare entry (added by uvl:sync) or old versioned entry if present
|
|
71
|
+
features = [
|
|
72
|
+
f for f in features if f != bare_name and not f.startswith(f"{bare_name}@")
|
|
73
|
+
]
|
|
74
|
+
features.append(full_name)
|
|
75
|
+
write_features_to_data(data, features)
|
|
76
|
+
with open(pyproject_path, "wb") as f:
|
|
77
|
+
tomli_w.dump(data, f)
|
|
78
|
+
click.echo(f"🧩 Updated pyproject.toml → {full_name}")
|
|
79
|
+
|
|
80
|
+
# --- 3️⃣ Create/update symlink ------------------------------------------
|
|
81
|
+
product_features_dir = os.path.join(product_path, "features", namespace_fs)
|
|
82
|
+
os.makedirs(product_features_dir, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
new_link = os.path.join(product_features_dir, f"{feature_name}@{version}")
|
|
85
|
+
if os.path.islink(new_link):
|
|
86
|
+
os.unlink(new_link)
|
|
87
|
+
rel_target = os.path.relpath(versioned_dir, product_features_dir)
|
|
88
|
+
os.symlink(rel_target, new_link)
|
|
89
|
+
|
|
90
|
+
click.echo(f"🔗 Linked {new_link} → {rel_target}")
|
|
91
|
+
|
|
92
|
+
# --- 4️⃣ Update manifest ------------------------------------------------
|
|
93
|
+
key = feature_key(namespace_fs, feature_name, version)
|
|
94
|
+
set_feature_state(
|
|
95
|
+
product_path,
|
|
96
|
+
product,
|
|
97
|
+
key,
|
|
98
|
+
"declared",
|
|
99
|
+
namespace=namespace_fs,
|
|
100
|
+
name=feature_name,
|
|
101
|
+
version=version,
|
|
102
|
+
mode="pinned",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
click.echo("🎯 Feature successfully attached.")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import subprocess
|
|
3
4
|
import requests
|
|
4
5
|
import click
|
|
@@ -22,7 +23,7 @@ def _get_latest_tag(namespace, repo) -> str | None:
|
|
|
22
23
|
r.raise_for_status()
|
|
23
24
|
tags = r.json()
|
|
24
25
|
return tags[0]["name"] if tags else None
|
|
25
|
-
except
|
|
26
|
+
except (requests.RequestException, KeyError, IndexError, ValueError):
|
|
26
27
|
return None
|
|
27
28
|
|
|
28
29
|
|
|
@@ -33,20 +34,26 @@ def _build_repo_url(namespace, repo):
|
|
|
33
34
|
1. If SPLENT_USE_SSH=true → SSH
|
|
34
35
|
2. Else if GITHUB_TOKEN exists → HTTPS with token
|
|
35
36
|
3. Else → HTTPS read-only
|
|
37
|
+
|
|
38
|
+
Returns a tuple (real_url, display_url) where display_url never contains a token.
|
|
36
39
|
"""
|
|
37
40
|
use_ssh = os.getenv("SPLENT_USE_SSH", "false").lower() == "true"
|
|
38
41
|
token = os.getenv("GITHUB_TOKEN")
|
|
39
42
|
|
|
40
43
|
if use_ssh:
|
|
41
44
|
click.secho("🔐 SSH mode enabled (SPLENT_USE_SSH=true)", fg="cyan")
|
|
42
|
-
|
|
45
|
+
url = f"git@github.com:{namespace}/{repo}.git"
|
|
46
|
+
return url, url
|
|
43
47
|
|
|
44
48
|
if token:
|
|
45
49
|
click.secho("🌐 HTTPS with token (SPLENT_USE_SSH not true)", fg="cyan")
|
|
46
|
-
|
|
50
|
+
real_url = f"https://{token}@github.com/{namespace}/{repo}.git"
|
|
51
|
+
display_url = f"https://github.com/{namespace}/{repo}.git"
|
|
52
|
+
return real_url, display_url
|
|
47
53
|
|
|
48
54
|
click.secho("🌍 HTTPS read-only (no token, no SSH)", fg="yellow")
|
|
49
|
-
|
|
55
|
+
url = f"https://github.com/{namespace}/{repo}.git"
|
|
56
|
+
return url, url
|
|
50
57
|
|
|
51
58
|
|
|
52
59
|
def _parse_full_name(full_name: str):
|
|
@@ -67,6 +74,11 @@ def _parse_full_name(full_name: str):
|
|
|
67
74
|
return namespace, repo, version
|
|
68
75
|
|
|
69
76
|
|
|
77
|
+
def _validate_identifier_part(value: str, label: str):
|
|
78
|
+
if not re.fullmatch(r'[a-zA-Z0-9_\-\.]+', value):
|
|
79
|
+
raise SystemExit(f"❌ Invalid {label}: '{value}'. Only letters, digits, - _ . allowed.")
|
|
80
|
+
|
|
81
|
+
|
|
70
82
|
# =====================================================================
|
|
71
83
|
# MAIN
|
|
72
84
|
# =====================================================================
|
|
@@ -88,6 +100,11 @@ def feature_clone(full_name):
|
|
|
88
100
|
|
|
89
101
|
namespace, repo, version = _parse_full_name(full_name)
|
|
90
102
|
|
|
103
|
+
_validate_identifier_part(namespace, "namespace")
|
|
104
|
+
_validate_identifier_part(repo, "repo")
|
|
105
|
+
if version:
|
|
106
|
+
_validate_identifier_part(version, "version")
|
|
107
|
+
|
|
91
108
|
if not version:
|
|
92
109
|
click.echo(
|
|
93
110
|
f"🔍 No version provided → fetching latest tag for {namespace}/{repo}..."
|
|
@@ -101,7 +118,7 @@ def feature_clone(full_name):
|
|
|
101
118
|
raise SystemExit(1)
|
|
102
119
|
|
|
103
120
|
# Build Git URL based on your ownership
|
|
104
|
-
fork_url = _build_repo_url(namespace, repo)
|
|
121
|
+
fork_url, display_url = _build_repo_url(namespace, repo)
|
|
105
122
|
|
|
106
123
|
# Local destination
|
|
107
124
|
namespace_safe = namespace.replace("-", "_").replace(".", "_")
|
|
@@ -115,7 +132,7 @@ def feature_clone(full_name):
|
|
|
115
132
|
click.secho(f"⚠️ Folder already exists: {local_path}", fg="yellow")
|
|
116
133
|
return
|
|
117
134
|
|
|
118
|
-
click.secho(f"⬇️ Cloning {
|
|
135
|
+
click.secho(f"⬇️ Cloning {display_url}@{version}", fg="cyan")
|
|
119
136
|
|
|
120
137
|
# Try clone specific tag/branch (suppress git noise)
|
|
121
138
|
try:
|
|
@@ -138,15 +155,25 @@ def feature_clone(full_name):
|
|
|
138
155
|
text=True,
|
|
139
156
|
)
|
|
140
157
|
except subprocess.CalledProcessError:
|
|
158
|
+
import shutil
|
|
159
|
+
shutil.rmtree(local_path, ignore_errors=True)
|
|
141
160
|
click.secho(
|
|
142
161
|
f"⚠️ Version '{version}' not found. Cloning main instead.", fg="yellow"
|
|
143
162
|
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
try:
|
|
164
|
+
subprocess.run(
|
|
165
|
+
["git", "clone", "--depth", "1", "--quiet", fork_url, local_path],
|
|
166
|
+
check=True,
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
)
|
|
170
|
+
except subprocess.CalledProcessError:
|
|
171
|
+
shutil.rmtree(local_path, ignore_errors=True)
|
|
172
|
+
click.secho(
|
|
173
|
+
f"❌ Repository '{namespace}/{repo}' not found or not accessible.",
|
|
174
|
+
fg="red",
|
|
175
|
+
)
|
|
176
|
+
raise SystemExit(1)
|
|
150
177
|
|
|
151
178
|
# Lock files as read-only to prevent accidental edits on pinned features
|
|
152
179
|
make_feature_readonly(local_path)
|
|
@@ -73,6 +73,14 @@ def make_feature(full_name):
|
|
|
73
73
|
return
|
|
74
74
|
|
|
75
75
|
# --- Jinja setup ---
|
|
76
|
+
templates_dir = PathUtils.get_splent_cli_templates_dir()
|
|
77
|
+
if not os.path.isdir(templates_dir):
|
|
78
|
+
click.secho(
|
|
79
|
+
f"❌ Templates directory not found: {templates_dir}\n"
|
|
80
|
+
" Ensure the CLI is installed correctly.",
|
|
81
|
+
fg="red",
|
|
82
|
+
)
|
|
83
|
+
raise SystemExit(1)
|
|
76
84
|
env = setup_jinja_env()
|
|
77
85
|
template_ctx = {
|
|
78
86
|
"feature_name": feature_name,
|
|
@@ -160,21 +168,22 @@ def make_feature(full_name):
|
|
|
160
168
|
|
|
161
169
|
# --- Permissions (UID:GID 1000:1000) ---
|
|
162
170
|
uid, gid = 1000, 1000
|
|
171
|
+
chown_failed = False
|
|
163
172
|
for root, dirs, files in os.walk(feature_dir):
|
|
164
173
|
try:
|
|
165
174
|
os.chown(root, uid, gid)
|
|
166
175
|
except PermissionError:
|
|
167
|
-
|
|
176
|
+
chown_failed = True
|
|
168
177
|
for d in dirs:
|
|
169
178
|
try:
|
|
170
179
|
os.chown(os.path.join(root, d), uid, gid)
|
|
171
180
|
except PermissionError:
|
|
172
|
-
|
|
181
|
+
chown_failed = True
|
|
173
182
|
for f in files:
|
|
174
183
|
try:
|
|
175
184
|
os.chown(os.path.join(root, f), uid, gid)
|
|
176
185
|
except PermissionError:
|
|
177
|
-
|
|
186
|
+
chown_failed = True
|
|
178
187
|
|
|
179
188
|
click.echo(
|
|
180
189
|
click.style(f"✅ Feature '{full_name}' created successfully!", fg="green")
|
|
@@ -182,5 +191,12 @@ def make_feature(full_name):
|
|
|
182
191
|
click.echo(click.style(f"📦 Created at: {feature_dir}", fg="blue"))
|
|
183
192
|
click.echo(click.style(f"🏷️ Namespace: {org_safe}", fg="bright_black"))
|
|
184
193
|
|
|
194
|
+
if chown_failed:
|
|
195
|
+
click.secho(
|
|
196
|
+
"⚠️ Could not set ownership 1000:1000 on some files.\n"
|
|
197
|
+
" If running outside the Docker container, this is expected and harmless.",
|
|
198
|
+
fg="yellow",
|
|
199
|
+
)
|
|
200
|
+
|
|
185
201
|
|
|
186
202
|
cli_command = make_feature
|
|
@@ -53,7 +53,7 @@ def feature_delete(feature_identifier, version, force):
|
|
|
53
53
|
try:
|
|
54
54
|
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
55
55
|
content = f.read()
|
|
56
|
-
except
|
|
56
|
+
except OSError:
|
|
57
57
|
continue
|
|
58
58
|
|
|
59
59
|
if f"{feature_name}@{version}" in content:
|
|
@@ -91,7 +91,7 @@ def feature_delete(feature_identifier, version, force):
|
|
|
91
91
|
try:
|
|
92
92
|
shutil.rmtree(cache_dir)
|
|
93
93
|
click.echo(f"🧹 Deleted: {cache_dir}")
|
|
94
|
-
except
|
|
94
|
+
except OSError as e:
|
|
95
95
|
click.echo(f"❌ Failed to delete: {e}")
|
|
96
96
|
raise SystemExit(1)
|
|
97
97
|
|
|
@@ -28,7 +28,7 @@ def _find_products_using_editable(workspace, feature_name, ns_safe):
|
|
|
28
28
|
try:
|
|
29
29
|
with open(pyproject_path, "rb") as f:
|
|
30
30
|
data = tomllib.load(f)
|
|
31
|
-
except
|
|
31
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
32
32
|
continue
|
|
33
33
|
|
|
34
34
|
features = read_features_from_data(data)
|
|
@@ -68,7 +68,7 @@ def feature_discard(feature_name, namespace):
|
|
|
68
68
|
click.echo(
|
|
69
69
|
f"ℹ️ No editable folder found for {feature_name}. Nothing to discard."
|
|
70
70
|
)
|
|
71
|
-
|
|
71
|
+
return
|
|
72
72
|
|
|
73
73
|
click.echo(f"🧩 Editable feature detected at:\n {editable_path}")
|
|
74
74
|
|