sanka-cli 0.2.2__tar.gz → 0.2.4__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.2 → sanka_cli-0.2.4}/PKG-INFO +20 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/README.md +19 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/pyproject.toml +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/cli/__init__.py +89 -26
- sanka_cli-0.2.4/src/sanka/cli/_compact.py +50 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/cli/_output.py +18 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/lifecycle.py +1 -2
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/store.py +31 -24
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/__init__.py +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/client.py +28 -1
- sanka_cli-0.2.4/src/sanka_cli/commands/cloud.py +240 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/main.py +2 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/runtime.py +8 -1
- sanka_cli-0.2.4/src/sanka_cli/skills/sanka-cli/SKILL.md +48 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_cli.py +68 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_cli_developer_experience.py +66 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_extension_lifecycle.py +1 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_extension_store.py +76 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_mcp.py +2 -2
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_sanka_facade.py +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_skill_commands.py +7 -1
- sanka_cli-0.2.2/src/sanka_cli/skills/sanka-cli/SKILL.md +0 -62
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/.gitignore +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/LICENSES/AGPL-3.0-only.txt +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/LICENSES/Apache-2.0.txt +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/NOTICE +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/cli/_research.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/cli/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/connector/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/connector/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/__about__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/concurrency.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/connector_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/connector_host.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/engine.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/continuous.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/local.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/report_codec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/routes.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/execution/validate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/discovery.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/extensions/runner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/hashing.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/mapping_candidates.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/owner_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/pending_relationships.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/mapping/record_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/planner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/private_sqlite.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/registry.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/safe_local_io.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/spec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka/runtime/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/__main__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/bundle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/ai.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/auth.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/cloud_migrate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/code.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/migrate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/profiles.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/resources.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/skill.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/commands/workflows.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/config.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/mcp/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/mcp/client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/mcp/render.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/mcp/server.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/output.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_cli/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/__about__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/credentials.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/protocols.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/provisioning.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/records.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/registration.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/src/sanka_connector/schema.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/conftest.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/api/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/api/serializers.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/api/views.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/config/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/config/settings.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/config/urls.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_drf_project/manage.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_marketplace/drf-to-fastapi.json +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/fixtures/extension_marketplace/marketplace.json +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_code_bundle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_code_commands.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_concurrency.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_connector_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_connector_host.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_credential_provider.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_engine_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_engine_execution_adoption.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_engine_pg_to_clickhouse_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_engine_postgres_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_continuous.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_exact_scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_local_store.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_report_codec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_routes.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_seam.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_execution_validate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_extension_discovery.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_extension_runner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_extension_wheel_acceptance.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_hashing.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_hosted_cli.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_mapping_candidates.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_mapping_owner_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_mapping_pending_relationships.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_mapping_record_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_planner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_registry.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_safe_local_io.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/tests/test_spec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.4}/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.4
|
|
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
|
|
@@ -149,3 +149,22 @@ The distribution license expression is
|
|
|
149
149
|
`Apache-2.0 AND AGPL-3.0-only`. Both license texts and the historical hosted
|
|
150
150
|
CLI notice ship in the wheel and source archive. File-level SPDX identifiers
|
|
151
151
|
are authoritative.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### Compact lifecycle output
|
|
155
|
+
|
|
156
|
+
Use `--compact-dsl` instead of `--json` on `scan`, `plan`, `apply`, `test`,
|
|
157
|
+
`verify`, and extension management commands when an agent needs a short result.
|
|
158
|
+
The flags are mutually exclusive; `--json` retains its existing contract.
|
|
159
|
+
Compact output starts with `sanka-compact/v1 <command> <outcome> <migration_state>`
|
|
160
|
+
then emits one `key=JSON-value` per line. Strings use JSON escaping, including
|
|
161
|
+
newlines and quotes. Failures, warnings, plan hashes, and limitations are retained.
|
|
162
|
+
Legacy aliases are not repeated. With full artifacts available, generated file
|
|
163
|
+
contents and route source code are replaced by explicit omission counts and file
|
|
164
|
+
names. Read the returned artifact for those details; a compact summary is not a
|
|
165
|
+
replacement for the full report. `data.plan_hash` becomes the `plan_hash` line;
|
|
166
|
+
use that core hash for apply, not a nested extension hash.
|
|
167
|
+
|
|
168
|
+
Keep the lifecycle: scan → plan → apply → test → verify. An applied scaffold with
|
|
169
|
+
manual gaps is not complete. Test establishes generated scope; scenario verification
|
|
170
|
+
checks HTTP and database parity within the supplied fixtures and cases.
|
|
@@ -119,3 +119,22 @@ The distribution license expression is
|
|
|
119
119
|
`Apache-2.0 AND AGPL-3.0-only`. Both license texts and the historical hosted
|
|
120
120
|
CLI notice ship in the wheel and source archive. File-level SPDX identifiers
|
|
121
121
|
are authoritative.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
### Compact lifecycle output
|
|
125
|
+
|
|
126
|
+
Use `--compact-dsl` instead of `--json` on `scan`, `plan`, `apply`, `test`,
|
|
127
|
+
`verify`, and extension management commands when an agent needs a short result.
|
|
128
|
+
The flags are mutually exclusive; `--json` retains its existing contract.
|
|
129
|
+
Compact output starts with `sanka-compact/v1 <command> <outcome> <migration_state>`
|
|
130
|
+
then emits one `key=JSON-value` per line. Strings use JSON escaping, including
|
|
131
|
+
newlines and quotes. Failures, warnings, plan hashes, and limitations are retained.
|
|
132
|
+
Legacy aliases are not repeated. With full artifacts available, generated file
|
|
133
|
+
contents and route source code are replaced by explicit omission counts and file
|
|
134
|
+
names. Read the returned artifact for those details; a compact summary is not a
|
|
135
|
+
replacement for the full report. `data.plan_hash` becomes the `plan_hash` line;
|
|
136
|
+
use that core hash for apply, not a nested extension hash.
|
|
137
|
+
|
|
138
|
+
Keep the lifecycle: scan → plan → apply → test → verify. An applied scaffold with
|
|
139
|
+
manual gaps is not complete. Test establishes generated scope; scenario verification
|
|
140
|
+
checks HTTP and database parity within the supplied fixtures and cases.
|
|
@@ -85,17 +85,21 @@ class _CliArgumentParser(argparse.ArgumentParser):
|
|
|
85
85
|
|
|
86
86
|
def main(argv: list[str] | None = None) -> int:
|
|
87
87
|
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
88
|
-
json_errors = "--json" in arguments
|
|
88
|
+
json_errors = "--json" in arguments or "--compact-dsl" in arguments
|
|
89
89
|
parser = _build_parser(json_errors=json_errors)
|
|
90
90
|
try:
|
|
91
91
|
args = parser.parse_args(arguments)
|
|
92
92
|
except CliUsageError as error:
|
|
93
93
|
command = arguments[0] if arguments and arguments[0] in SDK_COMMANDS else "sanka"
|
|
94
94
|
return _print_cli_error(
|
|
95
|
-
argparse.Namespace(
|
|
95
|
+
argparse.Namespace(
|
|
96
|
+
command=command, json=json_errors, compact_dsl="--compact-dsl" in arguments
|
|
97
|
+
),
|
|
96
98
|
error,
|
|
97
99
|
exit_code=2,
|
|
98
100
|
)
|
|
101
|
+
if getattr(args, "compact_dsl", False):
|
|
102
|
+
args.json = True
|
|
99
103
|
if args.command is None:
|
|
100
104
|
parser.print_help()
|
|
101
105
|
return 0
|
|
@@ -173,12 +177,17 @@ def _json_result(
|
|
|
173
177
|
return envelope
|
|
174
178
|
|
|
175
179
|
|
|
176
|
-
def
|
|
177
|
-
|
|
180
|
+
def _print_result(args: argparse.Namespace, payload: dict[str, Any]) -> None:
|
|
181
|
+
if getattr(args, "compact_dsl", False):
|
|
182
|
+
from sanka.cli._compact import render_compact
|
|
183
|
+
|
|
184
|
+
print(render_compact(payload))
|
|
185
|
+
else:
|
|
186
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
178
187
|
|
|
179
188
|
|
|
180
189
|
def _print_cli_error(args: argparse.Namespace, error: Exception, *, exit_code: int) -> int:
|
|
181
|
-
if getattr(args, "json", False):
|
|
190
|
+
if getattr(args, "json", False) or getattr(args, "compact_dsl", False):
|
|
182
191
|
code = getattr(error, "code", None)
|
|
183
192
|
details = getattr(error, "details", None)
|
|
184
193
|
structured_error: dict[str, Any] = {
|
|
@@ -187,13 +196,14 @@ def _print_cli_error(args: argparse.Namespace, error: Exception, *, exit_code: i
|
|
|
187
196
|
}
|
|
188
197
|
if isinstance(details, dict) and details:
|
|
189
198
|
structured_error["details"] = details
|
|
190
|
-
|
|
199
|
+
_print_result(
|
|
200
|
+
args,
|
|
191
201
|
_json_result(
|
|
192
202
|
str(getattr(args, "command", "sanka") or "sanka"),
|
|
193
203
|
{"error": structured_error},
|
|
194
204
|
outcome="error",
|
|
195
205
|
migration_state="not_started" if exit_code == 2 else "failed",
|
|
196
|
-
)
|
|
206
|
+
),
|
|
197
207
|
)
|
|
198
208
|
else:
|
|
199
209
|
_terminal(args).failure(str(error))
|
|
@@ -221,9 +231,15 @@ def _build_parser(*, json_errors: bool = False) -> argparse.ArgumentParser:
|
|
|
221
231
|
|
|
222
232
|
def presentation(sub: argparse.ArgumentParser, *, json_option: bool = True) -> None:
|
|
223
233
|
if json_option:
|
|
224
|
-
sub.
|
|
234
|
+
formats = sub.add_mutually_exclusive_group()
|
|
235
|
+
formats.add_argument(
|
|
225
236
|
"--json", action="store_true", help="print one sanka-cli/v1 JSON document"
|
|
226
237
|
)
|
|
238
|
+
formats.add_argument(
|
|
239
|
+
"--compact-dsl",
|
|
240
|
+
action="store_true",
|
|
241
|
+
help="print a compact result; full generated code stays in artifacts",
|
|
242
|
+
)
|
|
227
243
|
sub.add_argument("--no-color", action="store_true", help="disable ANSI color")
|
|
228
244
|
detail = sub.add_mutually_exclusive_group()
|
|
229
245
|
detail.add_argument("--quiet", action="store_true", help="print only outcome and errors")
|
|
@@ -414,7 +430,7 @@ def _build_parser(*, json_errors: bool = False) -> argparse.ArgumentParser:
|
|
|
414
430
|
description=(
|
|
415
431
|
"Check hashes, generated files, route coverage, and configured read-only HTTP "
|
|
416
432
|
"comparisons. With --scenarios, replay a scenario file against the source "
|
|
417
|
-
"application and a candidate FastAPI app and diff status, body, declared "
|
|
433
|
+
"application and a candidate FastAPI or Flask app and diff status, body, declared "
|
|
418
434
|
"headers, and database state; that mode needs no plan or generated manifest."
|
|
419
435
|
),
|
|
420
436
|
epilog=(
|
|
@@ -436,6 +452,7 @@ def _build_parser(*, json_errors: bool = False) -> argparse.ArgumentParser:
|
|
|
436
452
|
help="application repository root",
|
|
437
453
|
)
|
|
438
454
|
verify.add_argument("--to", help="select an application plan")
|
|
455
|
+
verify.add_argument("--settings", help="source Django settings module for scenario replay")
|
|
439
456
|
verify.add_argument("--artifact-dir", default=DEFAULT_ARTIFACT_DIR)
|
|
440
457
|
verify.add_argument("--output", default=None, help="generated FastAPI output directory")
|
|
441
458
|
verify.add_argument(
|
|
@@ -667,11 +684,53 @@ def _extension_result(
|
|
|
667
684
|
records: list[dict[str, Any]],
|
|
668
685
|
) -> int:
|
|
669
686
|
data = {"operation": operation, "records": records}
|
|
670
|
-
if args.json:
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
687
|
+
if args.json or getattr(args, "compact_dsl", False):
|
|
688
|
+
_print_result(args, _json_result("extension", data, migration_state="not_started"))
|
|
689
|
+
return 0
|
|
690
|
+
|
|
691
|
+
terminal = _terminal(args)
|
|
692
|
+
if operation == "list":
|
|
693
|
+
terminal.heading("Extensions")
|
|
694
|
+
terminal.table(
|
|
695
|
+
("Extension", "Kind", "Version", "Status", "Marketplace"),
|
|
696
|
+
[
|
|
697
|
+
(
|
|
698
|
+
str(record.get("id", "")),
|
|
699
|
+
str(record.get("kind", "")),
|
|
700
|
+
str(record.get("version", "")),
|
|
701
|
+
", ".join(map(str, record.get("status", []))),
|
|
702
|
+
str(record.get("marketplace", "")),
|
|
703
|
+
)
|
|
704
|
+
for record in records
|
|
705
|
+
],
|
|
706
|
+
)
|
|
707
|
+
elif operation == "marketplace_list":
|
|
708
|
+
terminal.heading("Marketplaces")
|
|
709
|
+
terminal.table(
|
|
710
|
+
("Marketplace", "Source", "Commit", "Trusted"),
|
|
711
|
+
[
|
|
712
|
+
(
|
|
713
|
+
str(record.get("name", "")),
|
|
714
|
+
str(record.get("source", "")),
|
|
715
|
+
str(record.get("resolved_commit") or "-")[:12],
|
|
716
|
+
"yes" if record.get("trusted") else "no",
|
|
717
|
+
)
|
|
718
|
+
for record in records
|
|
719
|
+
],
|
|
720
|
+
)
|
|
721
|
+
elif operation == "add":
|
|
722
|
+
record = records[0]
|
|
723
|
+
terminal.success(f"Installed {record['id']} {record['version']}")
|
|
724
|
+
elif operation == "remove":
|
|
725
|
+
terminal.success(f"Removed {records[0]['id']}")
|
|
726
|
+
elif operation == "marketplace_add":
|
|
727
|
+
terminal.success(f"Added marketplace {records[0]['name']}")
|
|
728
|
+
elif operation == "marketplace_upgrade":
|
|
729
|
+
terminal.success(
|
|
730
|
+
"Updated marketplace " + ", ".join(str(record["name"]) for record in records)
|
|
731
|
+
)
|
|
732
|
+
elif operation == "marketplace_remove":
|
|
733
|
+
terminal.success(f"Removed marketplace {records[0]['name']}")
|
|
675
734
|
return 0
|
|
676
735
|
|
|
677
736
|
|
|
@@ -853,8 +912,9 @@ def _print_application_result(
|
|
|
853
912
|
*,
|
|
854
913
|
migration_state: str,
|
|
855
914
|
) -> int:
|
|
856
|
-
if args.json:
|
|
857
|
-
|
|
915
|
+
if args.json or getattr(args, "compact_dsl", False):
|
|
916
|
+
_print_result(
|
|
917
|
+
args,
|
|
858
918
|
_json_result(
|
|
859
919
|
command,
|
|
860
920
|
result.data,
|
|
@@ -862,7 +922,7 @@ def _print_application_result(
|
|
|
862
922
|
artifacts=list(result.artifacts),
|
|
863
923
|
limitations=list(result.limitations),
|
|
864
924
|
next_actions=list(result.next_actions),
|
|
865
|
-
)
|
|
925
|
+
),
|
|
866
926
|
)
|
|
867
927
|
else:
|
|
868
928
|
terminal = _terminal(args)
|
|
@@ -884,8 +944,9 @@ async def _cmd_plan(args: argparse.Namespace) -> int:
|
|
|
884
944
|
engine = _engine(args.state)
|
|
885
945
|
run_id = engine.create(spec)
|
|
886
946
|
migration_plan = await engine.plan(run_id)
|
|
887
|
-
if args.json:
|
|
888
|
-
|
|
947
|
+
if args.json or getattr(args, "compact_dsl", False):
|
|
948
|
+
_print_result(
|
|
949
|
+
args,
|
|
889
950
|
_json_result(
|
|
890
951
|
"plan",
|
|
891
952
|
{
|
|
@@ -911,7 +972,7 @@ async def _cmd_plan(args: argparse.Namespace) -> int:
|
|
|
911
972
|
]
|
|
912
973
|
)
|
|
913
974
|
],
|
|
914
|
-
)
|
|
975
|
+
),
|
|
915
976
|
)
|
|
916
977
|
else:
|
|
917
978
|
_print_plan(run_id, migration_plan)
|
|
@@ -953,8 +1014,9 @@ async def _cmd_apply(args: argparse.Namespace) -> int:
|
|
|
953
1014
|
if run.plan_json is None:
|
|
954
1015
|
raise ExecutionError("no plan for this spec yet; run `sanka plan` first")
|
|
955
1016
|
await engine.apply(run_id, plan_hash=args.plan_hash)
|
|
956
|
-
if args.json:
|
|
957
|
-
|
|
1017
|
+
if args.json or getattr(args, "compact_dsl", False):
|
|
1018
|
+
_print_result(
|
|
1019
|
+
args,
|
|
958
1020
|
_json_result(
|
|
959
1021
|
"apply",
|
|
960
1022
|
{
|
|
@@ -975,7 +1037,7 @@ async def _cmd_apply(args: argparse.Namespace) -> int:
|
|
|
975
1037
|
]
|
|
976
1038
|
)
|
|
977
1039
|
],
|
|
978
|
-
)
|
|
1040
|
+
),
|
|
979
1041
|
)
|
|
980
1042
|
else:
|
|
981
1043
|
print(f"run {run_id}: applied")
|
|
@@ -1011,8 +1073,9 @@ async def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
1011
1073
|
engine = _engine(args.state)
|
|
1012
1074
|
run_id = engine.create(spec)
|
|
1013
1075
|
verify_report = await engine.verify(run_id)
|
|
1014
|
-
if args.json:
|
|
1015
|
-
|
|
1076
|
+
if args.json or getattr(args, "compact_dsl", False):
|
|
1077
|
+
_print_result(
|
|
1078
|
+
args,
|
|
1016
1079
|
_json_result(
|
|
1017
1080
|
"verify",
|
|
1018
1081
|
{
|
|
@@ -1035,7 +1098,7 @@ async def _cmd_verify(args: argparse.Namespace) -> int:
|
|
|
1035
1098
|
"verified_within_scope" if verify_report.ok else "verification_failed"
|
|
1036
1099
|
),
|
|
1037
1100
|
artifacts=[str(Path(args.state).resolve())],
|
|
1038
|
-
)
|
|
1101
|
+
),
|
|
1039
1102
|
)
|
|
1040
1103
|
else:
|
|
1041
1104
|
_print_verify(verify_report)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
"""Model-facing presentation of the existing result, with JSON-escaped values."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _summary(value: Any, *, path: tuple[str, ...] = (), artifacts: bool = False) -> Any:
|
|
11
|
+
if isinstance(value, dict):
|
|
12
|
+
if path in {("files",), ("extension", "files")} and artifacts:
|
|
13
|
+
return {"names": sorted(value), "omitted_file_contents": len(value)}
|
|
14
|
+
result = {}
|
|
15
|
+
for name, item in value.items():
|
|
16
|
+
if name == "extension" and isinstance(item, dict):
|
|
17
|
+
# Core lifecycle responses repeat extension fields; retain differing hashes.
|
|
18
|
+
item = {k: v for k, v in item.items() if k not in value or value[k] != v}
|
|
19
|
+
if not item:
|
|
20
|
+
continue
|
|
21
|
+
if (
|
|
22
|
+
name == "source"
|
|
23
|
+
and {"classification", "method"} <= value.keys()
|
|
24
|
+
and isinstance(item, str)
|
|
25
|
+
and artifacts
|
|
26
|
+
):
|
|
27
|
+
result["omitted_source_characters"] = len(item)
|
|
28
|
+
else:
|
|
29
|
+
result[name] = _summary(item, path=(*path, name), artifacts=artifacts)
|
|
30
|
+
return result
|
|
31
|
+
if isinstance(value, list):
|
|
32
|
+
return [_summary(item, path=(*path, "[]"), artifacts=artifacts) for item in value]
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def render_compact(payload: dict[str, Any]) -> str:
|
|
37
|
+
"""One header, then key=JSON lines. Never truncate failures, warnings or hashes."""
|
|
38
|
+
lines = [" ".join(str(payload[k]) for k in ("command", "outcome", "migration_state"))]
|
|
39
|
+
lines[0] = "sanka-compact/v1 " + lines[0]
|
|
40
|
+
data = _summary(payload["data"], artifacts=bool(payload.get("artifacts")))
|
|
41
|
+
for key, value in data.items():
|
|
42
|
+
# Escape even extension-defined keys so a newline cannot forge another record.
|
|
43
|
+
label = key if key.isidentifier() else json.dumps(key, ensure_ascii=False)
|
|
44
|
+
lines.append(label + "=" + json.dumps(value, ensure_ascii=False, separators=(",", ":")))
|
|
45
|
+
for key in ("artifacts", "limitations", "next_actions"):
|
|
46
|
+
if payload.get(key):
|
|
47
|
+
lines.append(
|
|
48
|
+
key + "=" + json.dumps(payload[key], ensure_ascii=False, separators=(",", ":"))
|
|
49
|
+
)
|
|
50
|
+
return "\n".join(lines)
|
|
@@ -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
|
|
|
@@ -820,7 +820,7 @@ class ApplicationLifecycle:
|
|
|
820
820
|
written, so the source fingerprint legitimately differs from any reviewed plan and
|
|
821
821
|
a plan may not exist yet. The extension lock still has to match its recommendation
|
|
822
822
|
exactly; only the plan, fingerprint, and artifact checks that guard apply and test
|
|
823
|
-
are skipped, because replay writes
|
|
823
|
+
are skipped, because replay only writes verification artifacts in its artifact directory.
|
|
824
824
|
"""
|
|
825
825
|
explicit_env_names = _canonical_environment_names(explicit_env_names)
|
|
826
826
|
fingerprint = fingerprint_repository(self.project_root)
|
|
@@ -839,7 +839,6 @@ class ApplicationLifecycle:
|
|
|
839
839
|
if result.outcome != "success":
|
|
840
840
|
self._raise_failure(result)
|
|
841
841
|
data = dict(result.data)
|
|
842
|
-
data["extension"] = result.data
|
|
843
842
|
return ExtensionResult(
|
|
844
843
|
outcome="success",
|
|
845
844
|
data=data,
|
|
@@ -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
|
|
|
@@ -87,16 +87,43 @@ class SankaApiClient:
|
|
|
87
87
|
path: str,
|
|
88
88
|
*,
|
|
89
89
|
params: dict[str, Any] | None = None,
|
|
90
|
+
headers: dict[str, str] | None = None,
|
|
91
|
+
max_bytes: int | None = None,
|
|
90
92
|
) -> tuple[bytes, dict[str, str]]:
|
|
91
93
|
"""Fetch a non-JSON body (currently only the Custom Code bundle download).
|
|
92
94
|
|
|
93
95
|
Returns headers alongside the payload so the caller can verify the digest the
|
|
94
96
|
server reports without a second request.
|
|
95
97
|
"""
|
|
98
|
+
request_headers = self._headers()
|
|
99
|
+
request_headers.update(headers or {})
|
|
100
|
+
if max_bytes is not None:
|
|
101
|
+
with self.client.stream(
|
|
102
|
+
method.upper(),
|
|
103
|
+
path,
|
|
104
|
+
headers=request_headers,
|
|
105
|
+
params=params,
|
|
106
|
+
) as response:
|
|
107
|
+
content = bytearray()
|
|
108
|
+
for chunk in response.iter_bytes(chunk_size=65536):
|
|
109
|
+
if len(content) + len(chunk) > max_bytes:
|
|
110
|
+
raise APIError(
|
|
111
|
+
status_code=413, message="API artifact exceeds its size limit"
|
|
112
|
+
)
|
|
113
|
+
content.extend(chunk)
|
|
114
|
+
if response.status_code >= 400:
|
|
115
|
+
self._raise_for_response(
|
|
116
|
+
httpx.Response(
|
|
117
|
+
response.status_code,
|
|
118
|
+
content=bytes(content),
|
|
119
|
+
headers=response.headers,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return bytes(content), dict(response.headers)
|
|
96
123
|
response = self.client.request(
|
|
97
124
|
method.upper(),
|
|
98
125
|
path,
|
|
99
|
-
headers=
|
|
126
|
+
headers=request_headers,
|
|
100
127
|
params=params,
|
|
101
128
|
)
|
|
102
129
|
if response.status_code >= 400:
|