sanka-cli 0.2.1__tar.gz → 0.2.3__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.3}/PKG-INFO +14 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/README.md +13 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/pyproject.toml +1 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/cli/__init__.py +45 -3
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/cli/_output.py +18 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/store.py +31 -24
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/__init__.py +1 -1
- sanka_cli-0.2.3/src/sanka_cli/commands/skill.py +146 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/main.py +2 -0
- sanka_cli-0.2.3/src/sanka_cli/skills/sanka-cli/SKILL.md +62 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_cli.py +68 -1
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_extension_store.py +76 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_mcp.py +2 -2
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_sanka_facade.py +1 -1
- sanka_cli-0.2.3/tests/test_skill_commands.py +219 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/.gitignore +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/LICENSES/AGPL-3.0-only.txt +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/LICENSES/Apache-2.0.txt +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/NOTICE +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/cli/_research.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/cli/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/connector/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/connector/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/__about__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/concurrency.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/connector_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/connector_host.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/engine.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/continuous.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/local.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/report_codec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/routes.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/execution/validate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/discovery.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/lifecycle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/runner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/hashing.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/mapping_candidates.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/model.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/owner_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/pending_relationships.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/record_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/planner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/private_sqlite.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/registry.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/safe_local_io.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/spec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka/runtime/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/__main__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/bundle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/ai.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/auth.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/cloud_migrate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/code.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/migrate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/profiles.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/resources.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/commands/workflows.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/config.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/mcp/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/mcp/client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/mcp/render.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/mcp/server.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/output.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/runtime.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_cli/state.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/__about__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/credentials.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/errors.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/protocols.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/provisioning.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/py.typed +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/records.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/registration.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/src/sanka_connector/schema.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/conftest.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/serializers.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/views.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/__init__.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/settings.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/urls.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/manage.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_marketplace/drf-to-fastapi.json +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/fixtures/extension_marketplace/marketplace.json +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_cli_developer_experience.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_code_bundle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_code_commands.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_concurrency.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_connector_client.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_connector_host.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_credential_provider.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_engine_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_engine_execution_adoption.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_engine_pg_to_clickhouse_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_engine_postgres_e2e.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_continuous.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_exact_scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_local_store.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_report_codec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_routes.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_scope.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_seam.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_execution_validate.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_extension_discovery.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_extension_lifecycle.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_extension_runner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_extension_wheel_acceptance.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_hashing.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_hosted_cli.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_mapping_candidates.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_mapping_owner_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_mapping_pending_relationships.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_mapping_record_mapping.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_planner.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_registry.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_safe_local_io.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/tests/test_spec.py +0 -0
- {sanka_cli-0.2.1 → sanka_cli-0.2.3}/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.3
|
|
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:
|
|
@@ -669,9 +669,51 @@ def _extension_result(
|
|
|
669
669
|
data = {"operation": operation, "records": records}
|
|
670
670
|
if args.json:
|
|
671
671
|
_print_json(_json_result("extension", data, migration_state="not_started"))
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
672
|
+
return 0
|
|
673
|
+
|
|
674
|
+
terminal = _terminal(args)
|
|
675
|
+
if operation == "list":
|
|
676
|
+
terminal.heading("Extensions")
|
|
677
|
+
terminal.table(
|
|
678
|
+
("Extension", "Kind", "Version", "Status", "Marketplace"),
|
|
679
|
+
[
|
|
680
|
+
(
|
|
681
|
+
str(record.get("id", "")),
|
|
682
|
+
str(record.get("kind", "")),
|
|
683
|
+
str(record.get("version", "")),
|
|
684
|
+
", ".join(map(str, record.get("status", []))),
|
|
685
|
+
str(record.get("marketplace", "")),
|
|
686
|
+
)
|
|
687
|
+
for record in records
|
|
688
|
+
],
|
|
689
|
+
)
|
|
690
|
+
elif operation == "marketplace_list":
|
|
691
|
+
terminal.heading("Marketplaces")
|
|
692
|
+
terminal.table(
|
|
693
|
+
("Marketplace", "Source", "Commit", "Trusted"),
|
|
694
|
+
[
|
|
695
|
+
(
|
|
696
|
+
str(record.get("name", "")),
|
|
697
|
+
str(record.get("source", "")),
|
|
698
|
+
str(record.get("resolved_commit") or "-")[:12],
|
|
699
|
+
"yes" if record.get("trusted") else "no",
|
|
700
|
+
)
|
|
701
|
+
for record in records
|
|
702
|
+
],
|
|
703
|
+
)
|
|
704
|
+
elif operation == "add":
|
|
705
|
+
record = records[0]
|
|
706
|
+
terminal.success(f"Installed {record['id']} {record['version']}")
|
|
707
|
+
elif operation == "remove":
|
|
708
|
+
terminal.success(f"Removed {records[0]['id']}")
|
|
709
|
+
elif operation == "marketplace_add":
|
|
710
|
+
terminal.success(f"Added marketplace {records[0]['name']}")
|
|
711
|
+
elif operation == "marketplace_upgrade":
|
|
712
|
+
terminal.success(
|
|
713
|
+
"Updated marketplace " + ", ".join(str(record["name"]) for record in records)
|
|
714
|
+
)
|
|
715
|
+
elif operation == "marketplace_remove":
|
|
716
|
+
terminal.success(f"Removed marketplace {records[0]['name']}")
|
|
675
717
|
return 0
|
|
676
718
|
|
|
677
719
|
|
|
@@ -57,6 +57,24 @@ class TerminalOutput:
|
|
|
57
57
|
if not self.quiet:
|
|
58
58
|
print(self.style(value, "cyan"), file=self.stdout)
|
|
59
59
|
|
|
60
|
+
def table(self, headers: tuple[str, ...], rows: list[tuple[str, ...]]) -> None:
|
|
61
|
+
if self.quiet:
|
|
62
|
+
return
|
|
63
|
+
widths = [
|
|
64
|
+
max(len(header), *(len(row[index]) for row in rows))
|
|
65
|
+
for index, header in enumerate(headers)
|
|
66
|
+
]
|
|
67
|
+
print(
|
|
68
|
+
" ".join(header.ljust(width) for header, width in zip(headers, widths, strict=True)),
|
|
69
|
+
file=self.stdout,
|
|
70
|
+
)
|
|
71
|
+
print(" ".join("-" * width for width in widths), file=self.stdout)
|
|
72
|
+
for row in rows:
|
|
73
|
+
print(
|
|
74
|
+
" ".join(value.ljust(width) for value, width in zip(row, widths, strict=True)),
|
|
75
|
+
file=self.stdout,
|
|
76
|
+
)
|
|
77
|
+
|
|
60
78
|
def success(self, value: str) -> None:
|
|
61
79
|
print(self.style(f"✓ OK {value}", "green"), file=self.stdout)
|
|
62
80
|
|
|
@@ -529,6 +529,13 @@ def _create_venv(
|
|
|
529
529
|
)
|
|
530
530
|
|
|
531
531
|
|
|
532
|
+
def _venv_launcher_names() -> tuple[str, ...]:
|
|
533
|
+
names = ("python", "python3", f"python{sys.version_info.major}.{sys.version_info.minor}")
|
|
534
|
+
if sys.version_info[:2] == (3, 14) and sys.getfilesystemencoding() == "utf-8":
|
|
535
|
+
names += ("𝜋thon",)
|
|
536
|
+
return names
|
|
537
|
+
|
|
538
|
+
|
|
532
539
|
def _normalize_venv_launchers(environment_descriptor: int) -> None:
|
|
533
540
|
if os.name == "nt":
|
|
534
541
|
return
|
|
@@ -543,10 +550,9 @@ def _normalize_venv_launchers(environment_descriptor: int) -> None:
|
|
|
543
550
|
)
|
|
544
551
|
os.unlink("lib64", dir_fd=environment_descriptor)
|
|
545
552
|
interpreter = Path(sys.executable).resolve()
|
|
546
|
-
names = ("python", "python3", f"python{sys.version_info.major}.{sys.version_info.minor}")
|
|
547
553
|
with _directory_at(environment_descriptor, "bin", Path("bin")) as bin_descriptor:
|
|
548
554
|
assert bin_descriptor is not None
|
|
549
|
-
for name in
|
|
555
|
+
for name in _venv_launcher_names():
|
|
550
556
|
with suppress(FileNotFoundError):
|
|
551
557
|
status = os.stat(name, dir_fd=bin_descriptor, follow_symlinks=False)
|
|
552
558
|
if not (stat.S_ISLNK(status.st_mode) or stat.S_ISREG(status.st_mode)):
|
|
@@ -2830,24 +2836,29 @@ class ExtensionStore:
|
|
|
2830
2836
|
missing_ok=True,
|
|
2831
2837
|
) as existing:
|
|
2832
2838
|
if existing is not None:
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
)
|
|
2841
|
-
if digest == expected_digest:
|
|
2842
|
-
try:
|
|
2843
|
-
self._verify_environment_artifacts(
|
|
2844
|
-
root, executable, wheels, providers=providers
|
|
2839
|
+
try:
|
|
2840
|
+
digest = content_hash(
|
|
2841
|
+
_tree_records(
|
|
2842
|
+
existing,
|
|
2843
|
+
error_code="SANKA_EXTENSION_PATH",
|
|
2844
|
+
subject="Extension environment",
|
|
2845
|
+
allowed_symlinks=self._environment_symlinks(),
|
|
2845
2846
|
)
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2847
|
+
)
|
|
2848
|
+
except ExtensionError as error:
|
|
2849
|
+
if error.code != "SANKA_EXTENSION_PATH" or expected_digest is not None:
|
|
2850
|
+
raise
|
|
2851
|
+
else:
|
|
2852
|
+
if digest == expected_digest:
|
|
2853
|
+
try:
|
|
2854
|
+
self._verify_environment_artifacts(
|
|
2855
|
+
root, executable, wheels, providers=providers
|
|
2856
|
+
)
|
|
2857
|
+
except ExtensionError as error:
|
|
2858
|
+
if error.code != "SANKA_EXTENSION_HASH_MISMATCH":
|
|
2859
|
+
raise
|
|
2860
|
+
else:
|
|
2861
|
+
return root
|
|
2851
2862
|
replace_existing = True
|
|
2852
2863
|
if replace_existing:
|
|
2853
2864
|
shutil.rmtree(environment_name, dir_fd=environments)
|
|
@@ -3212,11 +3223,7 @@ class ExtensionStore:
|
|
|
3212
3223
|
def _environment_symlinks() -> dict[str, Path]:
|
|
3213
3224
|
interpreter = Path(sys.executable).resolve()
|
|
3214
3225
|
return dict.fromkeys(
|
|
3215
|
-
(
|
|
3216
|
-
"bin/python",
|
|
3217
|
-
"bin/python3",
|
|
3218
|
-
f"bin/python{sys.version_info.major}.{sys.version_info.minor}",
|
|
3219
|
-
),
|
|
3226
|
+
(f"bin/{name}" for name in _venv_launcher_names()),
|
|
3220
3227
|
interpreter,
|
|
3221
3228
|
)
|
|
3222
3229
|
|
|
@@ -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.
|
|
@@ -163,6 +163,73 @@ def test_extension_management_uses_stable_json_envelopes(
|
|
|
163
163
|
]
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def test_extension_lists_use_human_readable_tables(
|
|
167
|
+
tmp_path: Path,
|
|
168
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
169
|
+
capsys: pytest.CaptureFixture[str],
|
|
170
|
+
) -> None:
|
|
171
|
+
class Record:
|
|
172
|
+
def __init__(self, data: dict[str, object]) -> None:
|
|
173
|
+
self.data = data
|
|
174
|
+
|
|
175
|
+
def to_dict(self) -> dict[str, object]:
|
|
176
|
+
return self.data
|
|
177
|
+
|
|
178
|
+
class Store:
|
|
179
|
+
def __init__(self, project_root: Path) -> None:
|
|
180
|
+
assert project_root == tmp_path
|
|
181
|
+
|
|
182
|
+
def list_extensions(self) -> tuple[Record, ...]:
|
|
183
|
+
return (
|
|
184
|
+
Record(
|
|
185
|
+
{
|
|
186
|
+
"id": "sanka/drf-to-fastapi",
|
|
187
|
+
"kind": "migration",
|
|
188
|
+
"version": "0.1.0a3",
|
|
189
|
+
"status": ["installed", "enabled"],
|
|
190
|
+
"marketplace": "official",
|
|
191
|
+
"wheels": [{"sha256": "secret-noise"}],
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def marketplaces(self) -> tuple[Record, ...]:
|
|
197
|
+
return (
|
|
198
|
+
Record(
|
|
199
|
+
{
|
|
200
|
+
"name": "official",
|
|
201
|
+
"source": "https://github.com/sankaHQ/extensions.git",
|
|
202
|
+
"resolved_commit": "a44ab1794924d45e8eeaeec456ce5c28c1c17d56",
|
|
203
|
+
"trusted": True,
|
|
204
|
+
"snapshot_root": "/private/cache/noise",
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
monkeypatch.chdir(tmp_path)
|
|
210
|
+
monkeypatch.setattr(cli, "ExtensionStore", Store)
|
|
211
|
+
|
|
212
|
+
assert main(["extension", "list", "--no-color"]) == 0
|
|
213
|
+
extensions = capsys.readouterr().out
|
|
214
|
+
assert "Extension" in extensions
|
|
215
|
+
assert "sanka/drf-to-fastapi" in extensions
|
|
216
|
+
assert "installed, enabled" in extensions
|
|
217
|
+
assert "wheels" not in extensions
|
|
218
|
+
assert "secret-noise" not in extensions
|
|
219
|
+
assert not extensions.lstrip().startswith("{")
|
|
220
|
+
|
|
221
|
+
assert main(["extension", "marketplace", "list", "--no-color"]) == 0
|
|
222
|
+
marketplaces = capsys.readouterr().out
|
|
223
|
+
assert "Marketplace" in marketplaces
|
|
224
|
+
assert "official" in marketplaces
|
|
225
|
+
assert "github.com/sankaHQ/extensions" in marketplaces
|
|
226
|
+
assert "a44ab179492" in marketplaces
|
|
227
|
+
assert "yes" in marketplaces
|
|
228
|
+
assert "snapshot_root" not in marketplaces
|
|
229
|
+
assert "private/cache/noise" not in marketplaces
|
|
230
|
+
assert not marketplaces.lstrip().startswith("{")
|
|
231
|
+
|
|
232
|
+
|
|
166
233
|
def test_extension_errors_preserve_stable_code_and_details(
|
|
167
234
|
tmp_path: Path,
|
|
168
235
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -550,7 +617,7 @@ def test_research_client_uses_public_branded_base_and_unwraps_data() -> None:
|
|
|
550
617
|
"after": ["2027-01"],
|
|
551
618
|
"locale": ["ja"],
|
|
552
619
|
}
|
|
553
|
-
assert request.get_header("User-agent") == "sanka-cli/0.2.
|
|
620
|
+
assert request.get_header("User-agent") == "sanka-cli/0.2.3"
|
|
554
621
|
assert timeout == 10.0
|
|
555
622
|
|
|
556
623
|
|
|
@@ -1748,6 +1748,82 @@ def test_venv_normalization_rejects_a_nonstandard_lib64_alias(tmp_path: Path) ->
|
|
|
1748
1748
|
os.close(descriptor)
|
|
1749
1749
|
|
|
1750
1750
|
|
|
1751
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1752
|
+
def test_venv_normalization_accepts_python_314_unicode_launcher(
|
|
1753
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1754
|
+
) -> None:
|
|
1755
|
+
environment = tmp_path / "environment"
|
|
1756
|
+
(environment / "bin").mkdir(parents=True)
|
|
1757
|
+
(environment / "bin" / "𝜋thon").symlink_to("python3.14")
|
|
1758
|
+
|
|
1759
|
+
class VersionInfo(tuple[object, ...]):
|
|
1760
|
+
major = 3
|
|
1761
|
+
minor = 14
|
|
1762
|
+
|
|
1763
|
+
version_info = VersionInfo((3, 14, 0, "final", 0))
|
|
1764
|
+
monkeypatch.setattr(sys, "version_info", version_info)
|
|
1765
|
+
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
|
|
1766
|
+
|
|
1767
|
+
descriptor = os.open(environment, os.O_RDONLY)
|
|
1768
|
+
try:
|
|
1769
|
+
extension_store._normalize_venv_launchers(descriptor)
|
|
1770
|
+
extension_store._tree_records(
|
|
1771
|
+
descriptor,
|
|
1772
|
+
error_code="SANKA_EXTENSION_PATH",
|
|
1773
|
+
subject="Extension environment",
|
|
1774
|
+
allowed_symlinks=ExtensionStore._environment_symlinks(),
|
|
1775
|
+
)
|
|
1776
|
+
finally:
|
|
1777
|
+
os.close(descriptor)
|
|
1778
|
+
|
|
1779
|
+
launcher = environment / "bin" / "𝜋thon"
|
|
1780
|
+
assert launcher.is_symlink()
|
|
1781
|
+
assert Path(os.readlink(launcher)) == Path(sys.executable).resolve()
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1785
|
+
def test_add_rebuilds_an_orphaned_environment_with_an_unverified_symlink(
|
|
1786
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1787
|
+
) -> None:
|
|
1788
|
+
source, wheel = _marketplace(tmp_path / "source")
|
|
1789
|
+
store = ExtensionStore(tmp_path / "project", user_root=tmp_path / "home")
|
|
1790
|
+
store.add_marketplace(source, name="fixtures", trust=True)
|
|
1791
|
+
manifest = load_marketplace(store.marketplaces()[0].snapshot_root)[0]
|
|
1792
|
+
_responses(monkeypatch, {manifest.wheels[0].name: wheel})
|
|
1793
|
+
environment = store.user_root / "environments" / store._manifest_artifact_digest(manifest)
|
|
1794
|
+
(environment / "bin").mkdir(parents=True)
|
|
1795
|
+
stale_launcher = environment / "bin" / "stale-python"
|
|
1796
|
+
stale_launcher.symlink_to("python")
|
|
1797
|
+
|
|
1798
|
+
lock = store.add_extension("example/demo")
|
|
1799
|
+
|
|
1800
|
+
assert lock.artifact_digest == environment.name
|
|
1801
|
+
assert not stale_launcher.exists() and not stale_launcher.is_symlink()
|
|
1802
|
+
assert store.resolve_locked("example/demo") == lock
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1806
|
+
def test_add_rejects_an_unverified_symlink_in_a_recorded_environment(
|
|
1807
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1808
|
+
) -> None:
|
|
1809
|
+
source, wheel = _marketplace(tmp_path / "source")
|
|
1810
|
+
store = ExtensionStore(tmp_path / "project", user_root=tmp_path / "home")
|
|
1811
|
+
store.add_marketplace(source, name="fixtures", trust=True)
|
|
1812
|
+
manifest = load_marketplace(store.marketplaces()[0].snapshot_root)[0]
|
|
1813
|
+
_responses(monkeypatch, {manifest.wheels[0].name: wheel})
|
|
1814
|
+
lock = store.add_extension("example/demo")
|
|
1815
|
+
stale_launcher = (
|
|
1816
|
+
store.user_root / "environments" / lock.artifact_digest / "bin" / "stale-python"
|
|
1817
|
+
)
|
|
1818
|
+
stale_launcher.symlink_to("python")
|
|
1819
|
+
|
|
1820
|
+
with pytest.raises(ExtensionError) as raised:
|
|
1821
|
+
store.add_extension("example/demo")
|
|
1822
|
+
|
|
1823
|
+
assert raised.value.code == "SANKA_EXTENSION_PATH"
|
|
1824
|
+
assert stale_launcher.is_symlink()
|
|
1825
|
+
|
|
1826
|
+
|
|
1751
1827
|
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1752
1828
|
def test_real_posix_materializer_installs_without_a_venv_test_hook(
|
|
1753
1829
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
@@ -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.3\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.3"
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def test_mcp_without_extra_has_install_hint(
|