Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fe0c548
feat:
jesvii Mar 17, 2026
89e17d6
Merge remote-tracking branch 'upstream/main'
jesvii Mar 23, 2026
53ccac5
chore(hooks): add commit-msg hook to main
jesvii Mar 23, 2026
27eccb6
ci: restore commits workflow
jesvii Mar 23, 2026
8169d1d
Merge remote-tracking branch 'upstream/main'
jesvii Apr 6, 2026
13d6f1f
feat: Add SPLENT API client
jesvii Apr 25, 2026
1e864be
feat: Use SPLENT API for feature search
jesvii Apr 25, 2026
716f70e
feat: Improve feature search with package API
jesvii Apr 26, 2026
4d4d16c
refactor: refactoring feature search
jesvii Apr 26, 2026
a2b6be1
feat: Use splent API in feature install
jesvii Apr 26, 2026
1c81b93
feat: Resolve feature versions through package API
jesvii Apr 26, 2026
d0237cb
feat: Use marketplace API for feature clone
jesvii Apr 26, 2026
6f1bfbf
feat: Add feature:info
jesvii Apr 26, 2026
17c7580
refactor: refactor feature search
jesvii Apr 26, 2026
2c874c0
feat: Implement `feature:publish` command to register features in the…
jesvii Apr 28, 2026
25adcd3
refactor: Allow publishing features without selected product
jesvii May 1, 2026
a93b389
refactor: change feature_ref by full_name
jesvii May 1, 2026
d6e6bdf
feat: gitignore
jesvii May 2, 2026
1f8f98c
feat: added splent_api_token to env.example and docker compose
jesvii May 2, 2026
45d72fe
fix: normalize marketplace package and repo URLs
jesvii May 4, 2026
e7b8a45
test: added tests (api client + feature search)
jesvii May 5, 2026
69a28e0
test: adding info, install and publish tests
jesvii May 5, 2026
beb1270
fix: Tests feature clone and clone cleanup fixed due to API relation now
jesvii May 5, 2026
3dd4f72
feat: added marketplace login and logout
jesvii May 6, 2026
c37d355
feat: added marketplace + edited search
jesvii May 6, 2026
7e4c3e4
feat: added validation
jesvii May 6, 2026
268160e
docs: updating readme
jesvii May 7, 2026
5b18043
feat: updating validation, login logout
jesvii May 10, 2026
803db15
feat: feature search and publish with login
jesvii May 11, 2026
1148e6e
feat: finishing the login and logout validation
jesvii May 11, 2026
22c9c0f
feat: Align marketplace login/logout flow with feature commands
jesvii May 11, 2026
16d6d33
feat: feature:install with dependencys, updated feature:remove also
jesvii May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ WORKING_DIR=/workspace
GITHUB_USER=
GITHUB_TOKEN=
TWINE_USERNAME=__token__
TWINE_PASSWORD=
TWINE_PASSWORD=
SPLENT_API_URL=http://host.docker.internal:5000
SPLENT_API_TOKEN=
29 changes: 29 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/sh
#
# Hook commit-msg para validar el formato de los mensajes de commit
# Se ejecuta automáticamente durante un `git commit`

set -eu

# Obtener el mensaje del commit
commit_subject=$(sed -n '1p' "$1")

# Ignorar ciertos tipos de commits automáticos
case "$commit_subject" in
Merge\ *|Revert\ *|Pull\ *|fixup\!*|squash\!*)
exit 0
;;
esac

# Expresión regular para el formato esperado en los mensajes
commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([a-z0-9._/-]+\))?: .{5,}$'

# Comprobar si el mensaje de commit cumple con el formato
if echo "$commit_subject" | grep -Eq "$commit_regex"; then
echo "✅ ¡El mensaje de commit es válido!"
else
echo "❌ El mensaje de commit no es válido."
echo "Formato esperado: tipo(alcance): descripcion"
echo "Ejemplo: feat(cli): add product env merge command"
exit 1
fi
22 changes: 22 additions & 0 deletions .github/workflows/ci-commits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Commits Syntax Checker

on:
push:
branches:
- '**'
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened

jobs:
check:
name: Conventional Commits
runs-on: ubuntu-24.04

steps:
- name: Validate commit messages
uses: webiny/action-conventional-commits@v1.3.0
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ build

__pycache__/
*.pyc
*.pyo
*.pyo
.env
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,102 @@ make setup # Prepare .env, start Docker, enter CLI container
splent --help # See all available commands
```

## Local marketplace flow

When using the CLI from inside the SPLENT Docker container, `127.0.0.1`
points to the container itself. If your marketplace/API is running on your
host machine, use `host.docker.internal`.

The marketplace/API must expose:

```text
GET /api/auth/check
POST /api/packages
GET /api/packages
```

For local development with token authentication, configure the API with:

```env
SPLENT_API_TOKEN=mi-token-secreto-local
```

Then, from the app workspace/container, log in:

```bash
splent marketplace:login --url http://host.docker.internal:5000 --token mi-token-secreto-local
```

`marketplace:login` requires a token. You can also put the token in `.env`
first and then run login without flags:

```env
SPLENT_API_URL=http://host.docker.internal:5000
SPLENT_API_TOKEN=mi-token-secreto-local
```

```bash
splent marketplace:login
```

This validates the token and saves the marketplace configuration in the
workspace `.env`:

```env
SPLENT_API_URL=http://host.docker.internal:5000
SPLENT_API_TOKEN=mi-token-secreto-local
SPLENT_MARKETPLACE_AUTH=true
```

`SPLENT_MARKETPLACE_AUTH=true` means the token has been validated with
`GET /api/auth/check`. Marketplace feature commands that read or write package
metadata require this validated login state and a configured `SPLENT_API_TOKEN`:

```text
feature:search
feature:info
feature:install
feature:clone
feature:versions
feature:publish
```

To log out:

```bash
splent marketplace:logout
```

Logout keeps `SPLENT_API_URL` and `SPLENT_API_TOKEN`, but marks the marketplace
session as inactive:

```env
SPLENT_API_URL=http://host.docker.internal:5000
SPLENT_API_TOKEN=mi-token-secreto-local
SPLENT_MARKETPLACE_AUTH=false
```

Run `splent marketplace:login` again to re-validate the saved token.

Create a feature and publish it:

```bash
splent feature:create splent-io/splent_feature_demo_marketplace --type light
splent feature:publish splent-io/splent_feature_demo_marketplace
```

Verify that it appears in the marketplace:

```bash
splent feature:search demo
```

You can also publish with an explicit version:

```bash
splent feature:publish splent-io/splent_feature_demo_marketplace@0.1.0
```

## Requirements

- Docker + Docker Compose
Expand Down
4 changes: 3 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ services:
build:
context: ../../
dockerfile: splent_cli/docker/images/Dockerfile.cli
env_file:
- ../.env
environment:
HOST_UID: ${HOST_UID:-1000}
HOST_GID: ${HOST_GID:-1000}
Expand All @@ -21,4 +23,4 @@ services:

networks:
splent_network:
name: splent_network
name: splent_network
4 changes: 4 additions & 0 deletions makefiles/Makefile.setup
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
ifeq ($(shell docker compose version >/dev/null 2>&1; echo $$?),0)
SPLENT_DOCKER_COMPOSE = docker compose -f docker/docker-compose.yml
else
SPLENT_DOCKER_COMPOSE = docker-compose -f docker/docker-compose.yml
endif
HOST_PROJECT_DIR := $(shell cd .. && pwd)

.PHONY: setup setup-rebuild env-prepare docker-up cli-enter
Expand Down
8 changes: 7 additions & 1 deletion scripts/setup_prompt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ splent() {
elif [ "$1" = "product:deselect" ]; then
shift
eval "$(command splent product:deselect --shell "$@")"
elif [ "$1" = "marketplace:login" ]; then
shift
eval "$(command splent marketplace:login --shell "$@")"
elif [ "$1" = "marketplace:logout" ]; then
shift
eval "$(command splent marketplace:logout --shell "$@")"
else
command splent "$@"
fi
Expand Down Expand Up @@ -80,4 +86,4 @@ _splent_completion() {
complete -o nosort -F _splent_completion splent

# --- END SPLENT CLI ---
EOF
EOF
40 changes: 29 additions & 11 deletions src/splent_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import os

import click
from dotenv import load_dotenv

from splent_cli.services.env import load_cli_env
from splent_cli.utils.dynamic_imports import get_app
from splent_cli.utils.command_loader import load_commands
from splent_cli.utils.db_utils import check_db_connection

load_dotenv()
load_cli_env()


class SPLENTCLI(click.Group):
Expand Down Expand Up @@ -48,9 +48,11 @@ def _load_feature_commands(self) -> dict[str, click.BaseCommand]:
group = click.Group(
name=f"feature:{feature_short}",
help=f"Commands contributed by splent_feature_{feature_short}.",
short_help=f"Subcommands: {cmd_names}"
if cmd_names
else f"Commands from splent_feature_{feature_short}.",
short_help=(
f"Subcommands: {cmd_names}"
if cmd_names
else f"Commands from splent_feature_{feature_short}."
),
)
group.requires_app = True # type: ignore[attr-defined]
for cmd in commands:
Expand All @@ -59,7 +61,9 @@ def _load_feature_commands(self) -> dict[str, click.BaseCommand]:
except Exception as e:
if os.getenv("SPLENT_DEBUG"):
click.secho(
f" ⚠ Feature commands not loaded: {e}", fg="yellow", err=True
f" ⚠ Feature commands not loaded: {e}",
fg="yellow",
err=True,
)
return self._feature_cmds_cache

Expand Down Expand Up @@ -98,10 +102,17 @@ def format_commands(self, ctx, formatter):
all_cmds = self.list_commands(ctx)
feat_cmds = self._load_feature_commands()
groups = {
"🛒 Marketplace": [
cmd
for cmd in all_cmds
if cmd.startswith("marketplace:") or cmd == "feature:search"
],
"🌿 Feature Management": [
cmd
for cmd in all_cmds
if cmd.startswith("feature:") and cmd not in feat_cmds
if cmd.startswith("feature:")
and cmd not in feat_cmds
and cmd != "feature:search"
],
"🏗️ Product Management": [
cmd for cmd in all_cmds if cmd.startswith("product:")
Expand All @@ -112,8 +123,12 @@ def format_commands(self, ctx, formatter):
"🧱 Database": [cmd for cmd in all_cmds if cmd.startswith("db:")],
"💾 Cache": [cmd for cmd in all_cmds if cmd.startswith("cache:")],
"🔍 Checks": [cmd for cmd in all_cmds if cmd.startswith("check:")],
"📦 Export": [cmd for cmd in all_cmds if cmd.startswith("export:")],
"🚀 Release": [cmd for cmd in all_cmds if cmd.startswith("release:")],
"📦 Export": [
cmd for cmd in all_cmds if cmd.startswith("export:")
],
"🚀 Release": [
cmd for cmd in all_cmds if cmd.startswith("release:")
],
"🧰 Utilities": [
cmd
for cmd in all_cmds
Expand All @@ -123,9 +138,12 @@ def format_commands(self, ctx, formatter):
"🐍 Development & QA": [
cmd
for cmd in all_cmds
if cmd in ("lint", "coverage", "selenium", "locust", "locust:stop")
if cmd
in ("lint", "coverage", "selenium", "locust", "locust:stop")
],
"🔌 Feature Commands": [
cmd for cmd in all_cmds if cmd in feat_cmds
],
"🔌 Feature Commands": [cmd for cmd in all_cmds if cmd in feat_cmds],
}
total = 0
for title, cmds in groups.items():
Expand Down
52 changes: 49 additions & 3 deletions src/splent_cli/commands/feature/feature_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import subprocess
import requests
import click
from splent_cli.services import context
from splent_cli.services import context, marketplace
from splent_cli.services.api_client import (
SplentAPIAuthError,
SplentAPIError,
get_package_by_name,
)
from splent_cli.utils.cache_utils import make_feature_readonly
from splent_cli.utils.feature_utils import normalize_namespace

Expand Down Expand Up @@ -69,6 +74,46 @@ def _parse_full_name(full_name: str):
return namespace, repo, version


def _feature_api_name(repo: str) -> str:
if repo.startswith("splent_feature_"):
return repo
return f"splent_feature_{repo}"


def _resolve_full_name_from_api(full_name: str) -> str:
raw = full_name
version = None
if "@" in raw:
raw, version = raw.split("@", 1)


repo = raw.split("/")[-1]
api_name = _feature_api_name(repo)

try:
marketplace.require_marketplace_login()
package = get_package_by_name(api_name)
except SplentAPIAuthError as exc:
click.secho(f"❌ {exc}", fg="red")
raise SystemExit(1) from exc
except SplentAPIError as exc:
click.secho(f"❌ {exc}", fg="red")
click.echo(" Check SPLENT_API_URL or start the package index.")
raise SystemExit(1) from exc

if not isinstance(package, dict):
click.secho("❌ Invalid package response from API.", fg="red")
raise SystemExit(1)

resolved = package.get("full_name")
if not isinstance(resolved, str) or "/" not in resolved:
resolved = f"splent-io/{package.get('name') or api_name}"

if version:
return f"{resolved}@{version}"
return resolved


def _validate_identifier_part(value: str, label: str):
if not re.fullmatch(r"[a-zA-Z0-9_\-\.]+", value):
raise SystemExit(
Expand All @@ -84,17 +129,18 @@ def _validate_identifier_part(value: str, label: str):
@click.command(
"feature:clone",
short_help="Clone a feature into the local cache.",
help="Clone a feature into the local cache namespace.",
help="Clone a marketplace feature into the local cache.",
)
@click.argument("full_name", required=True)
def feature_clone(full_name):
"""
Clone <namespace>/<repo> into:
Clone <feature>, <repo> or <namespace>/<repo> into:
.splent_cache/features/<namespace>/<repo>@<version>

- If version is omitted, fetches the latest tag or 'main'.
"""

full_name = _resolve_full_name_from_api(full_name)
namespace, repo, version = _parse_full_name(full_name)

_validate_identifier_part(namespace, "namespace")
Expand Down
Loading