sanka-cli 0.2.1__tar.gz → 0.2.2__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.
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/PKG-INFO +14 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/README.md +13 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/pyproject.toml +1 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/__init__.py +1 -1
- sanka_cli-0.2.2/src/sanka_cli/commands/skill.py +146 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/main.py +2 -0
- sanka_cli-0.2.2/src/sanka_cli/skills/sanka-cli/SKILL.md +62 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_cli.py +1 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_mcp.py +2 -2
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_sanka_facade.py +1 -1
- sanka_cli-0.2.2/tests/test_skill_commands.py +219 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/.gitignore +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/LICENSES/AGPL-3.0-only.txt +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/LICENSES/Apache-2.0.txt +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/NOTICE +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/cli/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/cli/_output.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/cli/_research.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/cli/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/connector/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/connector/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/__about__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/concurrency.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/connector_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/connector_host.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/engine.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/continuous.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/local.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/report_codec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/routes.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/execution/validate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/discovery.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/lifecycle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/runner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/extensions/store.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/hashing.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/mapping_candidates.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/owner_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/pending_relationships.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/mapping/record_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/planner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/private_sqlite.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/registry.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/safe_local_io.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/spec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka/runtime/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/__main__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/bundle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/ai.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/auth.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/cloud_migrate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/code.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/migrate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/profiles.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/resources.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/commands/workflows.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/config.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/mcp/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/mcp/client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/mcp/render.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/mcp/server.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/output.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/runtime.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_cli/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/__about__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/credentials.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/protocols.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/provisioning.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/records.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/registration.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/src/sanka_connector/schema.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/conftest.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/api/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/api/serializers.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/api/views.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/config/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/config/settings.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/config/urls.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_drf_project/manage.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_marketplace/drf-to-fastapi.json +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_marketplace/marketplace.json +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_cli_developer_experience.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_code_bundle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_code_commands.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_concurrency.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_connector_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_connector_host.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_credential_provider.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_engine_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_engine_execution_adoption.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_engine_pg_to_clickhouse_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_engine_postgres_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_continuous.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_exact_scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_local_store.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_report_codec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_routes.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_seam.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_execution_validate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_extension_discovery.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_extension_lifecycle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_extension_runner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_extension_store.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_extension_wheel_acceptance.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_hashing.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_hosted_cli.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_mapping_candidates.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_mapping_owner_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_mapping_pending_relationships.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_mapping_record_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_planner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_registry.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_safe_local_io.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_spec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/test_state_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sanka-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Sanka command-line interface and migration runtime
|
|
5
5
|
Project-URL: Repository, https://github.com/sankaHQ/sanka
|
|
6
6
|
Project-URL: Issues, https://github.com/sankaHQ/sanka/issues
|
|
@@ -54,6 +54,19 @@ uv tool install 'sanka-cli[mcp]'
|
|
|
54
54
|
sanka mcp
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
Install the bundled `sanka-cli` skill into an AI coding harness:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
sanka skill install claude
|
|
61
|
+
sanka skill install codex --scope project
|
|
62
|
+
sanka skill install --scope global
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The command prompts for project or global scope when `--scope` is omitted.
|
|
66
|
+
Without a harness argument, it installs into every detected Claude Code or
|
|
67
|
+
Codex CLI. Existing matching installs are left unchanged; different content
|
|
68
|
+
requires `--force`.
|
|
69
|
+
|
|
57
70
|
The base tool does not install framework migrations or provider drivers.
|
|
58
71
|
Components are immutable GitHub release wheels selected through the extension
|
|
59
72
|
manager:
|
|
@@ -24,6 +24,19 @@ uv tool install 'sanka-cli[mcp]'
|
|
|
24
24
|
sanka mcp
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Install the bundled `sanka-cli` skill into an AI coding harness:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sanka skill install claude
|
|
31
|
+
sanka skill install codex --scope project
|
|
32
|
+
sanka skill install --scope global
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The command prompts for project or global scope when `--scope` is omitted.
|
|
36
|
+
Without a harness argument, it installs into every detected Claude Code or
|
|
37
|
+
Codex CLI. Existing matching installs are left unchanged; different content
|
|
38
|
+
requires `--force`.
|
|
39
|
+
|
|
27
40
|
The base tool does not install framework migrations or provider drivers.
|
|
28
41
|
Components are immutable GitHub release wheels selected through the extension
|
|
29
42
|
manager:
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
import sanka_cli.runtime as runtime
|
|
14
|
+
from sanka_cli.state import CLIState
|
|
15
|
+
|
|
16
|
+
SKILL_NAME = "sanka-cli"
|
|
17
|
+
HARNESSES = ("claude", "codex")
|
|
18
|
+
CONFIG_ROOTS = {
|
|
19
|
+
"claude": ("CLAUDE_CONFIG_DIR", ".claude"),
|
|
20
|
+
"codex": ("CODEX_HOME", ".codex"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _skill_content() -> bytes:
|
|
25
|
+
return resources.files("sanka_cli").joinpath("skills", SKILL_NAME, "SKILL.md").read_bytes()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _target_directory(harness: str, scope: str, project_directory: Path) -> Path:
|
|
29
|
+
environment_name, default_directory = CONFIG_ROOTS[harness]
|
|
30
|
+
if scope == "project":
|
|
31
|
+
root = project_directory.resolve() / f".{harness}"
|
|
32
|
+
else:
|
|
33
|
+
configured_root = os.environ.get(environment_name)
|
|
34
|
+
root = (
|
|
35
|
+
Path(configured_root).expanduser()
|
|
36
|
+
if configured_root
|
|
37
|
+
else Path.home() / default_directory
|
|
38
|
+
)
|
|
39
|
+
return root / "skills" / SKILL_NAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _reject_project_symlinks(project_directory: Path, target: Path) -> None:
|
|
43
|
+
current = project_directory.resolve()
|
|
44
|
+
for part in (*target.relative_to(current).parts, "SKILL.md"):
|
|
45
|
+
current /= part
|
|
46
|
+
if current.is_symlink():
|
|
47
|
+
raise click.ClickException(
|
|
48
|
+
f"Refusing project installation through symbolic link: {current}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _planned_status(target: Path, content: bytes, *, force: bool) -> str:
|
|
53
|
+
skill_file = target / "SKILL.md"
|
|
54
|
+
try:
|
|
55
|
+
if skill_file.is_file() and skill_file.read_bytes() == content:
|
|
56
|
+
return "unchanged"
|
|
57
|
+
if target.exists() and not force:
|
|
58
|
+
raise click.ClickException(
|
|
59
|
+
f"{target} already exists with different content; "
|
|
60
|
+
"rerun with --force to replace SKILL.md."
|
|
61
|
+
)
|
|
62
|
+
return "updated" if target.exists() else "installed"
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
raise click.ClickException(f"Could not inspect {target}: {exc}") from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _write_skill(target: Path, content: bytes) -> None:
|
|
68
|
+
temporary_path: Path | None = None
|
|
69
|
+
try:
|
|
70
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
with tempfile.NamedTemporaryFile(
|
|
72
|
+
mode="wb", dir=target, prefix=".SKILL.md.", delete=False
|
|
73
|
+
) as file:
|
|
74
|
+
file.write(content)
|
|
75
|
+
temporary_path = Path(file.name)
|
|
76
|
+
os.replace(temporary_path, target / "SKILL.md")
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
raise click.ClickException(f"Could not install {SKILL_NAME} at {target}: {exc}") from exc
|
|
79
|
+
finally:
|
|
80
|
+
if temporary_path is not None:
|
|
81
|
+
temporary_path.unlink(missing_ok=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@click.group()
|
|
85
|
+
def skill() -> None:
|
|
86
|
+
"""Install Sanka skills into supported AI coding harnesses."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@skill.command("install")
|
|
90
|
+
@click.argument("harness", required=False, type=click.Choice(HARNESSES))
|
|
91
|
+
@click.option("--scope", type=click.Choice(["project", "global"]), default=None)
|
|
92
|
+
@click.option(
|
|
93
|
+
"--project-dir",
|
|
94
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
95
|
+
default=Path("."),
|
|
96
|
+
show_default=True,
|
|
97
|
+
)
|
|
98
|
+
@click.option("--force", is_flag=True, help="Replace a different installed SKILL.md.")
|
|
99
|
+
@click.pass_obj
|
|
100
|
+
def install_skill(
|
|
101
|
+
state: CLIState,
|
|
102
|
+
harness: str | None,
|
|
103
|
+
scope: str | None,
|
|
104
|
+
project_dir: Path,
|
|
105
|
+
force: bool,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Install the sanka-cli skill for Claude Code, Codex, or detected harnesses."""
|
|
108
|
+
harnesses = [harness] if harness else [name for name in HARNESSES if shutil.which(name)]
|
|
109
|
+
if not harnesses:
|
|
110
|
+
raise click.ClickException(
|
|
111
|
+
"No supported harness detected. Pass `claude` or `codex` explicitly."
|
|
112
|
+
)
|
|
113
|
+
resolved_scope = scope or click.prompt(
|
|
114
|
+
"Install scope",
|
|
115
|
+
type=click.Choice(["project", "global"]),
|
|
116
|
+
default="project",
|
|
117
|
+
err=True,
|
|
118
|
+
)
|
|
119
|
+
content = _skill_content()
|
|
120
|
+
plans: list[tuple[str, Path, str]] = []
|
|
121
|
+
for harness_name in harnesses:
|
|
122
|
+
target = _target_directory(harness_name, resolved_scope, project_dir)
|
|
123
|
+
if resolved_scope == "project":
|
|
124
|
+
_reject_project_symlinks(project_dir, target)
|
|
125
|
+
plans.append((harness_name, target, _planned_status(target, content, force=force)))
|
|
126
|
+
|
|
127
|
+
installations = []
|
|
128
|
+
for harness_name, target, status in plans:
|
|
129
|
+
if status != "unchanged":
|
|
130
|
+
_write_skill(target, content)
|
|
131
|
+
installations.append(
|
|
132
|
+
{
|
|
133
|
+
"harness": harness_name,
|
|
134
|
+
"path": str(target),
|
|
135
|
+
"status": status,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
runtime.emit_payload(
|
|
139
|
+
{
|
|
140
|
+
"skill": SKILL_NAME,
|
|
141
|
+
"scope": resolved_scope,
|
|
142
|
+
"content_sha256": hashlib.sha256(content).hexdigest(),
|
|
143
|
+
"installations": installations,
|
|
144
|
+
},
|
|
145
|
+
state,
|
|
146
|
+
)
|
|
@@ -10,6 +10,7 @@ from sanka_cli.commands.code import code
|
|
|
10
10
|
from sanka_cli.commands.migrate import register_migration_passthroughs
|
|
11
11
|
from sanka_cli.commands.profiles import profiles
|
|
12
12
|
from sanka_cli.commands.resources import attach_resource_group
|
|
13
|
+
from sanka_cli.commands.skill import skill
|
|
13
14
|
from sanka_cli.commands.workflows import workflows
|
|
14
15
|
from sanka_cli.output import print_error
|
|
15
16
|
from sanka_cli.state import CLIState
|
|
@@ -43,6 +44,7 @@ cli.add_command(profiles)
|
|
|
43
44
|
cli.add_command(workflows)
|
|
44
45
|
cli.add_command(ai)
|
|
45
46
|
cli.add_command(code)
|
|
47
|
+
cli.add_command(skill)
|
|
46
48
|
|
|
47
49
|
attach_resource_group(cli, "companies", "/v2/public/companies")
|
|
48
50
|
attach_resource_group(cli, "contacts", "/v2/public/contacts")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sanka-cli
|
|
3
|
+
description: Use when inspecting, planning, applying, testing, verifying, or monitoring application and data migrations with the sanka command-line interface.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sanka CLI
|
|
7
|
+
|
|
8
|
+
Use `sanka` for application modernization and data movement. Derive the migration mode and destination from the user's task; never assume FastAPI or any other target.
|
|
9
|
+
|
|
10
|
+
## Choose the migration mode
|
|
11
|
+
|
|
12
|
+
Run `sanka --help`, the selected command's `--help`, and `sanka extension list --json` before choosing options. Do not guess target names, component IDs, or flags.
|
|
13
|
+
|
|
14
|
+
- **Application migration:** a source repository is converted to a task-selected framework or runtime. Use target-specific flags only when the task and command help require them.
|
|
15
|
+
- **Data migration:** records move between task-selected systems described by `sanka.yaml`. Use environment references for credentials and install only the required trusted extensions after authorization.
|
|
16
|
+
|
|
17
|
+
## Application migration
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sanka scan . --json
|
|
21
|
+
sanka plan . --json
|
|
22
|
+
sanka apply --root . --plan-hash <reviewed-plan-hash> --json
|
|
23
|
+
sanka test . --json
|
|
24
|
+
sanka verify . --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add task-specific plan, output, or target options from `sanka plan --help`; the sequence does not imply a particular destination.
|
|
28
|
+
|
|
29
|
+
## Data migration
|
|
30
|
+
|
|
31
|
+
Define the source and target in `sanka.yaml`. This is the basic shape; use exact types and options reported by installed extensions:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
source:
|
|
35
|
+
type: <source-type>
|
|
36
|
+
connection: <path-or-environment-reference>
|
|
37
|
+
target:
|
|
38
|
+
type: <target-type>
|
|
39
|
+
connection: <path-or-environment-reference>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
sanka plan --json
|
|
46
|
+
sanka validate --json
|
|
47
|
+
sanka apply --plan-hash <reviewed-plan-hash> --json
|
|
48
|
+
sanka verify --json
|
|
49
|
+
sanka status
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`validate` checks source records against the plan without writing to the destination. Review its result before applying. Use `sanka extension --help` when a required source, target, or migration capability is not installed.
|
|
53
|
+
|
|
54
|
+
## Safety boundary
|
|
55
|
+
|
|
56
|
+
- Planning must not write to the destination. Review the source, target, mappings, readiness, limitations, output path, and exact `plan_hash`.
|
|
57
|
+
- Get explicit user authorization immediately before `apply`; apply only the reviewed hash.
|
|
58
|
+
- Treat source behavior and the task contract as authoritative. Generated applications require parity verification; data `apply` performs real destination writes.
|
|
59
|
+
- If pre-write testing or validation fails, fix the plan or configuration and re-plan. If post-write verification fails, inspect `status`; never rerun `apply` blindly. Any changed plan requires review and authorization of its new hash.
|
|
60
|
+
- Never silently switch between local and hosted execution. Use the configured profile only when the task selects hosted operation.
|
|
61
|
+
- Use `--bench-candidate <path>` on `sanka apply` only when a benchmark task requests that artifact layout.
|
|
62
|
+
- Do not expose credentials in command arguments, logs, generated files, or reports.
|
|
@@ -550,7 +550,7 @@ def test_research_client_uses_public_branded_base_and_unwraps_data() -> None:
|
|
|
550
550
|
"after": ["2027-01"],
|
|
551
551
|
"locale": ["ja"],
|
|
552
552
|
}
|
|
553
|
-
assert request.get_header("User-agent") == "sanka-cli/0.2.
|
|
553
|
+
assert request.get_header("User-agent") == "sanka-cli/0.2.2"
|
|
554
554
|
assert timeout == 10.0
|
|
555
555
|
|
|
556
556
|
|
|
@@ -21,7 +21,7 @@ def test_root_version_uses_unified_public_version(runner: CliRunner) -> None:
|
|
|
21
21
|
result = runner.invoke(cli, ["--version"])
|
|
22
22
|
|
|
23
23
|
assert result.exit_code == 0
|
|
24
|
-
assert result.output == "sanka, version 0.2.
|
|
24
|
+
assert result.output == "sanka, version 0.2.2\n"
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_mcp_client_uses_the_cli_version_in_its_user_agent() -> None:
|
|
@@ -29,7 +29,7 @@ def test_mcp_client_uses_the_cli_version_in_its_user_agent() -> None:
|
|
|
29
29
|
|
|
30
30
|
client = SankaMigrateApiClient()
|
|
31
31
|
|
|
32
|
-
assert client._client.headers["User-Agent"] == "sanka-cli/0.2.
|
|
32
|
+
assert client._client.headers["User-Agent"] == "sanka-cli/0.2.2"
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def test_mcp_without_extra_has_install_hint(
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from sanka_cli.main import cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def runner() -> CliRunner:
|
|
15
|
+
return CliRunner()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_install_claude_skill_in_project(runner: CliRunner, tmp_path: Path) -> None:
|
|
19
|
+
result = runner.invoke(
|
|
20
|
+
cli,
|
|
21
|
+
[
|
|
22
|
+
"--output",
|
|
23
|
+
"json",
|
|
24
|
+
"skill",
|
|
25
|
+
"install",
|
|
26
|
+
"claude",
|
|
27
|
+
"--scope",
|
|
28
|
+
"project",
|
|
29
|
+
"--project-dir",
|
|
30
|
+
str(tmp_path),
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert result.exit_code == 0, result.output
|
|
35
|
+
skill_file = tmp_path / ".claude" / "skills" / "sanka-cli" / "SKILL.md"
|
|
36
|
+
assert skill_file.is_file()
|
|
37
|
+
assert skill_file.read_text().startswith("---\nname: sanka-cli\n")
|
|
38
|
+
assert json.loads(result.output)["installations"] == [
|
|
39
|
+
{
|
|
40
|
+
"harness": "claude",
|
|
41
|
+
"path": str(skill_file.parent),
|
|
42
|
+
"status": "installed",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize(
|
|
48
|
+
("harness", "environment_name", "config_directory"),
|
|
49
|
+
[
|
|
50
|
+
("claude", "CLAUDE_CONFIG_DIR", ".claude-test"),
|
|
51
|
+
("codex", "CODEX_HOME", ".codex-test"),
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
def test_install_skill_globally(
|
|
55
|
+
runner: CliRunner,
|
|
56
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
57
|
+
tmp_path: Path,
|
|
58
|
+
harness: str,
|
|
59
|
+
environment_name: str,
|
|
60
|
+
config_directory: str,
|
|
61
|
+
) -> None:
|
|
62
|
+
config_root = tmp_path / config_directory
|
|
63
|
+
monkeypatch.setenv(environment_name, str(config_root))
|
|
64
|
+
|
|
65
|
+
result = runner.invoke(
|
|
66
|
+
cli,
|
|
67
|
+
["--output", "json", "skill", "install", harness, "--scope", "global"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert result.exit_code == 0, result.output
|
|
71
|
+
assert (config_root / "skills" / "sanka-cli" / "SKILL.md").is_file()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_install_prompts_for_scope(runner: CliRunner, tmp_path: Path) -> None:
|
|
75
|
+
result = runner.invoke(
|
|
76
|
+
cli,
|
|
77
|
+
["--output", "json", "skill", "install", "codex", "--project-dir", str(tmp_path)],
|
|
78
|
+
input="project\n",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert result.exit_code == 0, result.output
|
|
82
|
+
assert "Install scope (project, global) [project]:" in result.stderr
|
|
83
|
+
assert json.loads(result.stdout)["scope"] == "project"
|
|
84
|
+
assert (tmp_path / ".codex" / "skills" / "sanka-cli" / "SKILL.md").is_file()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_install_without_harness_detects_available_harnesses(
|
|
88
|
+
runner: CliRunner,
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
90
|
+
tmp_path: Path,
|
|
91
|
+
) -> None:
|
|
92
|
+
bin_directory = tmp_path / "bin"
|
|
93
|
+
bin_directory.mkdir()
|
|
94
|
+
for executable_name in ("claude", "codex"):
|
|
95
|
+
executable = bin_directory / executable_name
|
|
96
|
+
executable.touch(mode=0o755)
|
|
97
|
+
monkeypatch.setenv("PATH", str(bin_directory))
|
|
98
|
+
|
|
99
|
+
result = runner.invoke(
|
|
100
|
+
cli,
|
|
101
|
+
[
|
|
102
|
+
"--output",
|
|
103
|
+
"json",
|
|
104
|
+
"skill",
|
|
105
|
+
"install",
|
|
106
|
+
"--scope",
|
|
107
|
+
"project",
|
|
108
|
+
"--project-dir",
|
|
109
|
+
str(tmp_path),
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert result.exit_code == 0, result.output
|
|
114
|
+
payload = json.loads(result.output)
|
|
115
|
+
assert [item["harness"] for item in payload["installations"]] == ["claude", "codex"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_install_is_idempotent_and_refuses_conflicts_without_force(
|
|
119
|
+
runner: CliRunner,
|
|
120
|
+
tmp_path: Path,
|
|
121
|
+
) -> None:
|
|
122
|
+
arguments = [
|
|
123
|
+
"--output",
|
|
124
|
+
"json",
|
|
125
|
+
"skill",
|
|
126
|
+
"install",
|
|
127
|
+
"claude",
|
|
128
|
+
"--scope",
|
|
129
|
+
"project",
|
|
130
|
+
"--project-dir",
|
|
131
|
+
str(tmp_path),
|
|
132
|
+
]
|
|
133
|
+
assert runner.invoke(cli, arguments).exit_code == 0
|
|
134
|
+
|
|
135
|
+
unchanged = runner.invoke(cli, arguments)
|
|
136
|
+
assert unchanged.exit_code == 0
|
|
137
|
+
assert json.loads(unchanged.output)["installations"][0]["status"] == "unchanged"
|
|
138
|
+
|
|
139
|
+
skill_file = tmp_path / ".claude" / "skills" / "sanka-cli" / "SKILL.md"
|
|
140
|
+
skill_file.write_text("local changes\n")
|
|
141
|
+
conflict = runner.invoke(cli, arguments)
|
|
142
|
+
assert conflict.exit_code == 1
|
|
143
|
+
assert "already exists with different content" in conflict.output
|
|
144
|
+
assert skill_file.read_text() == "local changes\n"
|
|
145
|
+
|
|
146
|
+
updated = runner.invoke(cli, [*arguments, "--force"])
|
|
147
|
+
assert updated.exit_code == 0
|
|
148
|
+
assert json.loads(updated.output)["installations"][0]["status"] == "updated"
|
|
149
|
+
assert skill_file.read_text().startswith("---\nname: sanka-cli\n")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_detected_harness_conflict_does_not_partially_install(
|
|
153
|
+
runner: CliRunner,
|
|
154
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
155
|
+
tmp_path: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
bin_directory = tmp_path / "bin"
|
|
158
|
+
bin_directory.mkdir()
|
|
159
|
+
for executable_name in ("claude", "codex"):
|
|
160
|
+
executable = bin_directory / executable_name
|
|
161
|
+
executable.touch(mode=0o755)
|
|
162
|
+
monkeypatch.setenv("PATH", str(bin_directory))
|
|
163
|
+
codex_skill = tmp_path / ".codex" / "skills" / "sanka-cli" / "SKILL.md"
|
|
164
|
+
codex_skill.parent.mkdir(parents=True)
|
|
165
|
+
codex_skill.write_text("local changes\n")
|
|
166
|
+
|
|
167
|
+
result = runner.invoke(
|
|
168
|
+
cli,
|
|
169
|
+
[
|
|
170
|
+
"skill",
|
|
171
|
+
"install",
|
|
172
|
+
"--scope",
|
|
173
|
+
"project",
|
|
174
|
+
"--project-dir",
|
|
175
|
+
str(tmp_path),
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
assert result.exit_code == 1
|
|
180
|
+
assert not (tmp_path / ".claude" / "skills" / "sanka-cli").exists()
|
|
181
|
+
assert codex_skill.read_text() == "local changes\n"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.parametrize("symlink_target", ["directory", "file"])
|
|
185
|
+
def test_project_install_refuses_symlinked_destination(
|
|
186
|
+
runner: CliRunner,
|
|
187
|
+
tmp_path: Path,
|
|
188
|
+
symlink_target: str,
|
|
189
|
+
) -> None:
|
|
190
|
+
outside_directory = tmp_path / "outside"
|
|
191
|
+
outside_directory.mkdir()
|
|
192
|
+
outside_file = outside_directory / "outside.md"
|
|
193
|
+
outside_file.write_text("keep me\n")
|
|
194
|
+
project = tmp_path / "project"
|
|
195
|
+
skill_file = project / ".claude" / "skills" / "sanka-cli" / "SKILL.md"
|
|
196
|
+
if symlink_target == "directory":
|
|
197
|
+
skill_file.parent.parent.mkdir(parents=True)
|
|
198
|
+
skill_file.parent.symlink_to(outside_directory, target_is_directory=True)
|
|
199
|
+
else:
|
|
200
|
+
skill_file.parent.mkdir(parents=True)
|
|
201
|
+
skill_file.symlink_to(outside_file)
|
|
202
|
+
|
|
203
|
+
result = runner.invoke(
|
|
204
|
+
cli,
|
|
205
|
+
[
|
|
206
|
+
"skill",
|
|
207
|
+
"install",
|
|
208
|
+
"claude",
|
|
209
|
+
"--scope",
|
|
210
|
+
"project",
|
|
211
|
+
"--project-dir",
|
|
212
|
+
str(project),
|
|
213
|
+
"--force",
|
|
214
|
+
],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
assert result.exit_code == 1
|
|
218
|
+
assert "symbolic link" in result.output
|
|
219
|
+
assert outside_file.read_text() == "keep me\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanka_cli-0.2.1 → sanka_cli-0.2.2}/tests/fixtures/extension_marketplace/drf-to-fastapi.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|