Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ kbagent config list [--project NAME] [--component-type TYPE] [--component-id ID]
kbagent config detail --project NAME --component-id ID --config-id ID [--branch ID]
kbagent config search --query PATTERN [--project NAME] [--component-type TYPE] [--ignore-case] [--regex] [--branch ID]
kbagent config update --project NAME --component-id ID --config-id ID [--name N] [--description D] [--configuration JSON|@file|-] [--configuration-file PATH] [--set PATH=VALUE ...] [--merge] [--dry-run] [--branch ID]
kbagent config rename --project NAME --component-id ID --config-id ID --name "New Name" [--branch ID] [--directory DIR]

kbagent job list [--project NAME] [--component-id ID] [--status STATUS] [--limit N]
kbagent job detail --project NAME --job-id ID
Expand Down
1 change: 1 addition & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ If kbagent is not installed or you need the full standalone reference, run `kbag
| Show detailed information about a specific configuration | `kbagent config detail --project PROJECT --component-id COMPONENT-ID --config-id CONFIG-ID` |
| Search through configuration bodies for a string or pattern | `kbagent config search --query QUERY` |
| Update a configuration's metadata and/or content | `kbagent config update --project PROJECT --component-id COMPONENT-ID --config-id CONFIG-ID` |
| Rename a configuration (update name via API + rename local sync directory) | `kbagent config rename --project PROJECT --component-id COMPONENT-ID --config-id CONFIG-ID --name NAME` |
| Delete a configuration from a project | `kbagent config delete --project PROJECT --component-id COMPONENT-ID --config-id CONFIG-ID` |
| Generate boilerplate configuration files for a Keboola component | `kbagent config new --component-id COMPONENT-ID` |
| List jobs from connected projects | `kbagent job list` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ All commands support `--json` for structured output. Multi-project flags (`--pro
- `config detail --project NAME --component-id ID --config-id ID [--branch ID]` -- full config with parameters and rows (branch-aware)
- `config search --query PATTERN [--project NAME] [-i] [-r] [--branch ID]` -- search config bodies for string/regex (branch-aware)
- `config update --project NAME --component-id ID --config-id ID [--name N] [--description D] [--configuration JSON|@file|-] [--configuration-file PATH] [--set PATH=VALUE ...] [--merge] [--dry-run] [--branch ID]` -- update metadata and/or configuration content. `--set` targets a nested key (e.g. `parameters.db.host=new-host`). `--merge` deep-merges into existing config (preserves sibling keys). `--dry-run` previews changes without applying. Paths are relative to the configuration root (unlike MCP's `update_config` which uses paths relative to `parameters`)
- `config rename --project NAME --component-id ID --config-id ID --name "New Name" [--branch ID] [--directory DIR]` -- rename a configuration (API update + local sync directory rename with git mv support)
- `config delete --project NAME --component-id ID --config-id ID [--branch ID]` -- delete a configuration
- `config new --component-id ID [--project NAME] [--name NAME] [--output-dir DIR]` -- scaffold new config from component schema

Expand Down
111 changes: 111 additions & 0 deletions src/keboola_agent_cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,117 @@ def config_update(
)


@config_app.command("rename")
def config_rename(
ctx: typer.Context,
project: str = typer.Option(
...,
"--project",
help="Project alias",
),
component_id: str = typer.Option(
...,
"--component-id",
help="Component ID (e.g. keboola.python-transformation-v2)",
),
config_id: str = typer.Option(
...,
"--config-id",
help="Configuration ID to rename",
),
name: str = typer.Option(
...,
"--name",
help="New name for the configuration",
),
branch: int | None = typer.Option(
None,
"--branch",
help="Rename in a specific dev branch ID (defaults to active branch)",
),
directory: Path | None = typer.Option(
None,
"--directory",
"-d",
help="Sync working directory (auto-detects .keboola/manifest.json in CWD if omitted)",
),
) -> None:
"""Rename a configuration (update name via API + rename local sync directory).

Updates the configuration name in the Keboola project. If a local sync
directory is detected (either via --directory or the current working
directory), the local folder is renamed and the manifest is updated
to match.

\b
Examples:
# Simple rename
kbagent config rename --project prod --component-id kds-team.app-custom-python \\
--config-id abc123 --name "Stripe Extractor"

# Rename with explicit sync directory
kbagent config rename --project prod --component-id kds-team.app-custom-python \\
--config-id abc123 --name "Stripe Extractor" --directory ./my-project
"""
if should_hint(ctx):
emit_hint(
ctx,
"config.rename",
project=project,
component_id=component_id,
config_id=config_id,
name=name,
branch=branch,
)

formatter = get_formatter(ctx)
service = get_service(ctx, "config_service")

# Auto-detect sync directory from CWD if not specified
effective_directory = directory
if effective_directory is None:
cwd = Path.cwd()
if (cwd / KEBOOLA_DIR_NAME / MANIFEST_FILENAME).exists():
effective_directory = cwd

try:
result = service.rename_config(
alias=project,
component_id=component_id,
config_id=config_id,
name=name,
branch_id=branch,
directory=effective_directory,
)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None
except KeboolaApiError as exc:
formatter.error(
message=exc.message,
error_code=exc.error_code,
retryable=exc.retryable,
)
raise typer.Exit(code=map_error_to_exit_code(exc)) from None

if formatter.json_mode:
formatter.output(result)
else:
branch_info = ""
if result.get("branch_id"):
branch_info = f" (branch {result['branch_id']})"
formatter.success(
f'Renamed "{result["old_name"]}" -> "{result["new_name"]}"'
f" ({component_id}/{config_id}){branch_info}"
)
sync_info = result.get("sync")
if sync_info:
formatter.console.print(
f" Sync: {sync_info['old_path']}/ -> {sync_info['new_path']}/"
f" ({sync_info['method']})"
)


@config_app.command("delete")
def config_delete(
ctx: typer.Context,
Expand Down
6 changes: 6 additions & 0 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
existing config (preserves sibling keys). --dry-run previews changes.
Paths are always relative to the configuration root.

kbagent config rename --project NAME --component-id ID --config-id ID --name "New Name" [--branch ID] [--directory DIR]
Rename a configuration. Updates name via API. If a local sync directory
exists (.keboola/manifest.json), renames the directory and updates the
manifest path. Uses git mv when inside a git repo for cleaner history.

kbagent config delete --project NAME --component-id ID --config-id ID [--branch ID]
Delete a configuration. Branch-aware.

Expand Down Expand Up @@ -276,6 +281,7 @@
Download configs as local files. Idempotent, protects local modifications.
--job-limit controls max recent jobs per config (default 5). For large projects,
automatically falls back to per-config job fetching to ensure all configs get job history.
Auto-detects renamed configs and renames local directories to match (uses git mv in git repos).

kbagent sync status [--directory DIR]
Show local changes since last pull (SHA256-based).
Expand Down
27 changes: 25 additions & 2 deletions src/keboola_agent_cli/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ def _format_pull_result(formatter: Any, result: dict) -> None:
new_cfgs = [d for d in details if d["action"] == "new"]
updated_cfgs = [d for d in details if d["action"] == "updated"]
removed_cfgs = [d for d in details if d["action"] == "removed"]
renamed_cfgs = [d for d in details if d["action"] == "renamed"]
skipped_cfgs = [d for d in details if d["action"] == "skipped"]

has_changes = bool(new_cfgs or updated_cfgs or removed_cfgs)
has_changes = bool(new_cfgs or updated_cfgs or removed_cfgs or renamed_cfgs)

storage = result.get("storage", {})
jobs_written = result.get("jobs_written", 0)
Expand Down Expand Up @@ -168,6 +169,12 @@ def _format_pull_result(formatter: Any, result: dict) -> None:
if jobs_written:
formatter.console.print(f" Jobs: {jobs_written} configs with job history")

if renamed_cfgs:
formatter.console.print(f" [magenta]Renamed ({len(renamed_cfgs)}):[/magenta]")
for d in renamed_cfgs:
formatter.console.print(
f" > {d.get('old_path', '?')} -> {d['component_id']}/{d['config_name']}"
)
if new_cfgs:
formatter.console.print(f" [green]New ({len(new_cfgs)}):[/green]")
for d in new_cfgs:
Expand Down Expand Up @@ -241,6 +248,19 @@ def _format_push_result(formatter: Any, result: dict) -> None:
f"{result.get('updated', 0)} updated, "
f"{result.get('deleted', 0)} deleted"
)
# Show name drift warnings
drift_warnings = result.get("name_drift_warnings", [])
if drift_warnings:
formatter.console.print(
f"\n [yellow]Warning: {len(drift_warnings)} config(s) have "
f"local directory names that don't match their config name:[/yellow]"
)
for w in drift_warnings:
formatter.console.print(
f" '{w['local_dirname']}' should be "
f"'{w['expected_dirname']}' (config: {w['config_name']})"
)
formatter.console.print(" Run 'kbagent config rename' or 'kbagent sync pull' to fix.")


def _pull_one_liner(result: dict) -> str:
Expand All @@ -249,10 +269,13 @@ def _pull_one_liner(result: dict) -> str:
new_n = sum(1 for d in details if d["action"] == "new")
upd_n = sum(1 for d in details if d["action"] == "updated")
rem_n = sum(1 for d in details if d["action"] == "removed")
ren_n = sum(1 for d in details if d["action"] == "renamed")
skip_n = sum(1 for d in details if d["action"] == "skipped")
if not new_n and not upd_n and not rem_n and not skip_n:
if not new_n and not upd_n and not rem_n and not ren_n and not skip_n:
return "[green]up to date[/green]"
parts = []
if ren_n:
parts.append(f"[magenta]>{ren_n} renamed[/magenta]")
if new_n:
parts.append(f"[green]+{new_n} new[/green]")
if upd_n:
Expand Down
44 changes: 43 additions & 1 deletion src/keboola_agent_cli/hints/definitions/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Hint definitions for config commands (list, detail, search)."""
"""Hint definitions for config commands (list, detail, search, rename)."""

from .. import HintRegistry
from ..models import ClientCall, CommandHint, HintStep, ServiceCall
Expand Down Expand Up @@ -118,3 +118,45 @@
],
)
)

# ── config rename ─────────────────────────────────────────────────

HintRegistry.register(
CommandHint(
cli_command="config.rename",
description="Rename a configuration (update name via API + local sync dir)",
steps=[
HintStep(
comment="Rename configuration via API",
client=ClientCall(
method="update_config",
args={
"component_id": "{component_id}",
"config_id": "{config_id}",
"name": "{name}",
"branch_id": "{branch}",
},
result_var="result",
result_hint="dict",
),
service=ServiceCall(
service_class="ConfigService",
service_module="config_service",
method="rename_config",
args={
"alias": "{project}",
"component_id": "{component_id}",
"config_id": "{config_id}",
"name": "{name}",
"branch_id": "{branch}",
},
),
),
],
notes=[
"Only the name is updated; configuration content is unchanged.",
"If a local sync directory exists, the folder is renamed and "
"manifest.json is updated automatically.",
],
)
)
1 change: 1 addition & 0 deletions src/keboola_agent_cli/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"config.detail": "read",
"config.search": "read",
"config.update": "write",
"config.rename": "write",
"config.delete": "destructive",
"config.new": "write",
# Job history
Expand Down
Loading