maxc-cli 0.3.2__tar.gz → 0.4.0__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.
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/PKG-INFO +1 -1
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/setup.py +1 -1
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/agent_platforms.py +5 -5
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/app.py +47 -13
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/query.py +2 -1
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/cli.py +36 -38
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/help_format.py +43 -37
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/SKILL.md +1 -8
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_agent_platforms.py +9 -10
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_agent_skill_commands.py +2 -2
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_agent_skill_commands_context.py +18 -18
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_cli_mock.py +37 -8
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_e2e_smoke.py +7 -7
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_flag_hoist.py +7 -2
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_help_format.py +17 -17
- maxc_cli-0.4.0/tests/test_help_version_e2e.py +144 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_phase1_improvements.py +27 -49
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/MANIFEST.in +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/README.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/pyproject.toml +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/scripts/regression_test.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/setup.cfg +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_cache.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_catalog.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_compat.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_helpers.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_integration.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_masking.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.3.2 → maxc_cli-0.4.0}/tests/test_skill_renderer.py +0 -0
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="maxc-cli",
|
|
12
|
-
version="0.
|
|
12
|
+
version="0.4.0",
|
|
13
13
|
description="Agent-native MaxCompute CLI for external coding agents",
|
|
14
14
|
long_description=README.read_text(encoding="utf-8"),
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -74,7 +74,7 @@ INVOCATIONS: dict[str, dict[str, str]] = {
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def _claude_root() -> Path:
|
|
77
|
-
return Path.home() / ".claude" / "
|
|
77
|
+
return Path.home() / ".claude" / "skills" / "maxc-cli"
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def _codex_root() -> Path:
|
|
@@ -83,12 +83,12 @@ def _codex_root() -> Path:
|
|
|
83
83
|
return (
|
|
84
84
|
Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex")))
|
|
85
85
|
/ "skills"
|
|
86
|
-
/ "
|
|
86
|
+
/ "maxc-cli"
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _simple_root(dotdir: str) -> Path:
|
|
91
|
-
return Path.home() / dotdir / "skills" / "
|
|
91
|
+
return Path.home() / dotdir / "skills" / "maxc-cli"
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def render_claude_plugin(install_dir: Path, cli: str, cli_module: str) -> None:
|
|
@@ -117,8 +117,8 @@ def _build_registry() -> tuple[Platform, ...]:
|
|
|
117
117
|
name="claude-code",
|
|
118
118
|
install_root=_claude_root(),
|
|
119
119
|
skill_subpath=None,
|
|
120
|
-
extra_files=(
|
|
121
|
-
next_step_hint="
|
|
120
|
+
extra_files=(),
|
|
121
|
+
next_step_hint="Restart Claude Code or run /reload-plugins to activate",
|
|
122
122
|
),
|
|
123
123
|
Platform(name="cursor", install_root=_simple_root(".cursor"),
|
|
124
124
|
next_step_hint="Restart Cursor to activate"),
|
|
@@ -2161,7 +2161,13 @@ class MaxCApp:
|
|
|
2161
2161
|
self.log("meta.list-schemas", envelope.status, envelope.metadata)
|
|
2162
2162
|
return envelope
|
|
2163
2163
|
|
|
2164
|
-
def session_set(
|
|
2164
|
+
def session_set(
|
|
2165
|
+
self,
|
|
2166
|
+
project: 'str | None' = None,
|
|
2167
|
+
schema: 'str | None' = None,
|
|
2168
|
+
*,
|
|
2169
|
+
target_config_path: 'Path | None' = None,
|
|
2170
|
+
) -> 'Envelope':
|
|
2165
2171
|
"""Set default project and/or schema by writing to ~/.maxc/config.yaml.
|
|
2166
2172
|
|
|
2167
2173
|
Mirrors `gcloud config set project` / `kubectl config use-context`: the
|
|
@@ -2169,8 +2175,12 @@ class MaxCApp:
|
|
|
2169
2175
|
(e.g., ./.maxc/config.yaml) shadows the value, a warning is emitted but
|
|
2170
2176
|
the write still happens — the in-memory value is updated for the current
|
|
2171
2177
|
invocation.
|
|
2178
|
+
|
|
2179
|
+
When ``target_config_path`` is given (i.e. the user passed ``--config``),
|
|
2180
|
+
the write goes to that file instead, so a subsequent ``session show
|
|
2181
|
+
--config <same>`` round-trips correctly.
|
|
2172
2182
|
"""
|
|
2173
|
-
target_path = default_global_config_path()
|
|
2183
|
+
target_path = target_config_path or default_global_config_path()
|
|
2174
2184
|
config_payload = load_config_mapping(target_path) if target_path.exists() else {}
|
|
2175
2185
|
|
|
2176
2186
|
changes: list[str] = []
|
|
@@ -2246,13 +2256,18 @@ class MaxCApp:
|
|
|
2246
2256
|
self.log("session.set", envelope.status, {"changes": changes})
|
|
2247
2257
|
return envelope
|
|
2248
2258
|
|
|
2249
|
-
def session_unset(
|
|
2259
|
+
def session_unset(
|
|
2260
|
+
self, *, target_config_path: 'Path | None' = None
|
|
2261
|
+
) -> 'Envelope':
|
|
2250
2262
|
"""Remove default_project / default_schema from ~/.maxc/config.yaml.
|
|
2251
2263
|
|
|
2252
2264
|
Project-level config files in the working directory are NOT modified, since
|
|
2253
2265
|
they may be checked into version control. Edit those by hand if needed.
|
|
2266
|
+
|
|
2267
|
+
When ``target_config_path`` is given (``--config``), unset operates on
|
|
2268
|
+
that file instead of the global one.
|
|
2254
2269
|
"""
|
|
2255
|
-
target_path = default_global_config_path()
|
|
2270
|
+
target_path = target_config_path or default_global_config_path()
|
|
2256
2271
|
cleared: list[str] = []
|
|
2257
2272
|
|
|
2258
2273
|
if target_path.exists():
|
|
@@ -2284,9 +2299,16 @@ class MaxCApp:
|
|
|
2284
2299
|
self.log("session.unset", envelope.status, {})
|
|
2285
2300
|
return envelope
|
|
2286
2301
|
|
|
2287
|
-
def session_show(
|
|
2288
|
-
|
|
2289
|
-
|
|
2302
|
+
def session_show(
|
|
2303
|
+
self, *, target_config_path: 'Path | None' = None
|
|
2304
|
+
) -> 'Envelope':
|
|
2305
|
+
"""Show current session settings with source information.
|
|
2306
|
+
|
|
2307
|
+
When ``target_config_path`` is given (``--config``), the reported
|
|
2308
|
+
``config_path`` reflects that file, matching what ``session set
|
|
2309
|
+
--config <same>`` would write to.
|
|
2310
|
+
"""
|
|
2311
|
+
config_path = target_config_path or default_global_config_path()
|
|
2290
2312
|
|
|
2291
2313
|
env_project = os.environ.get("MAXCOMPUTE_PROJECT") or os.environ.get("ODPS_PROJECT")
|
|
2292
2314
|
has_explicit_auth_provider = bool(self.config.auth.provider)
|
|
@@ -3469,6 +3491,16 @@ class MaxCApp:
|
|
|
3469
3491
|
|
|
3470
3492
|
return sorted(files_copied)
|
|
3471
3493
|
|
|
3494
|
+
_LEGACY_SKILL_DIRS = ("maxcompute-cli-guidance", "use-maxc-cli")
|
|
3495
|
+
|
|
3496
|
+
def _cleanup_legacy_skill_dir(self, target: 'Path') -> None:
|
|
3497
|
+
"""Remove legacy skill directories that have been superseded by the new path."""
|
|
3498
|
+
import shutil
|
|
3499
|
+
for old_name in self._LEGACY_SKILL_DIRS:
|
|
3500
|
+
old_dir = target.parent / old_name
|
|
3501
|
+
if old_dir.is_dir() and (old_dir / ".maxc-skill-version").is_file():
|
|
3502
|
+
shutil.rmtree(str(old_dir))
|
|
3503
|
+
|
|
3472
3504
|
def skill_install(
|
|
3473
3505
|
self,
|
|
3474
3506
|
*,
|
|
@@ -3478,14 +3510,16 @@ class MaxCApp:
|
|
|
3478
3510
|
force: 'bool' = False,
|
|
3479
3511
|
) -> 'Envelope':
|
|
3480
3512
|
from . import agent_platforms
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3513
|
+
# invocation is now the literal cli name (e.g. "maxc", "aliyun maxc")
|
|
3514
|
+
# For backwards compat, also accept legacy key "aliyun-maxc"
|
|
3515
|
+
if invocation in agent_platforms.INVOCATIONS:
|
|
3516
|
+
invocation_map = agent_platforms.INVOCATIONS[invocation]
|
|
3517
|
+
else:
|
|
3518
|
+
invocation_map = {"cli": invocation, "cli_module": invocation}
|
|
3486
3519
|
platform_spec, target = self._resolve_skill_target(platform, dir_override)
|
|
3487
|
-
invocation_map = agent_platforms.INVOCATIONS[invocation]
|
|
3488
3520
|
skills_src = self._locate_skills_source()
|
|
3521
|
+
if dir_override is None:
|
|
3522
|
+
self._cleanup_legacy_skill_dir(target)
|
|
3489
3523
|
version_marker = f"{__version__}+{invocation}"
|
|
3490
3524
|
marker_path = target / ".maxc-skill-version"
|
|
3491
3525
|
if not force and marker_path.is_file() and marker_path.read_text().strip() == version_marker:
|
|
@@ -356,13 +356,14 @@ class QueryMixin:
|
|
|
356
356
|
|
|
357
357
|
actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
|
|
358
358
|
priority_kwargs = {"priority": priority} if priority is not None else {}
|
|
359
|
+
idem_kwargs = {"unique_identifier_id": idempotency_key} if idempotency_key is not None else {}
|
|
359
360
|
|
|
360
361
|
try:
|
|
361
362
|
instance = self.client.run_sql(
|
|
362
363
|
actual_sql,
|
|
363
364
|
project=project,
|
|
364
365
|
hints=hints,
|
|
365
|
-
|
|
366
|
+
**idem_kwargs,
|
|
366
367
|
**priority_kwargs,
|
|
367
368
|
)
|
|
368
369
|
except Exception as exc:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import difflib
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Sequence, TextIO
|
|
@@ -75,6 +76,9 @@ def _epilog_for(command_path: str) -> str | None:
|
|
|
75
76
|
sample = SAMPLES.get(command_path)
|
|
76
77
|
if sample is None:
|
|
77
78
|
return None
|
|
79
|
+
cli_name = os.environ.get("MAXC_CLI_NAME", "").strip() or "maxc"
|
|
80
|
+
if cli_name != "maxc":
|
|
81
|
+
sample = sample.replace("maxc ", f"{cli_name} ")
|
|
78
82
|
return "Sample:\n " + sample.replace("\n", "\n ")
|
|
79
83
|
|
|
80
84
|
|
|
@@ -82,6 +86,10 @@ def _epilog_for(command_path: str) -> str | None:
|
|
|
82
86
|
# (e.g., --project, --limit) are NOT hoisted — they belong to specific
|
|
83
87
|
# subparsers. arity = number of subsequent argv tokens consumed as a value
|
|
84
88
|
# when given as `--flag value`; `--flag=value` is always a single token.
|
|
89
|
+
#
|
|
90
|
+
# -h/--help is deliberately NOT hoisted: argparse auto-adds it to every
|
|
91
|
+
# subparser, and hoisting would turn `maxc query -h` into top-level help
|
|
92
|
+
# instead of the query subcommand's help.
|
|
85
93
|
_GLOBAL_FLAG_ARITY: dict[str, int] = {
|
|
86
94
|
"--format": 1,
|
|
87
95
|
"-f": 1,
|
|
@@ -92,8 +100,6 @@ _GLOBAL_FLAG_ARITY: dict[str, int] = {
|
|
|
92
100
|
"--debug": 0,
|
|
93
101
|
"-v": 0,
|
|
94
102
|
"--version": 0,
|
|
95
|
-
"--help": 0,
|
|
96
|
-
"-h": 0,
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
|
|
@@ -134,11 +140,8 @@ def _hoist_global_flags(argv: list[str]) -> list[str]:
|
|
|
134
140
|
|
|
135
141
|
|
|
136
142
|
def _make_parser(parent_subparsers, name, command_path, **kw):
|
|
137
|
-
"""Wrap add_parser so every parser gets aliyun-style formatting
|
|
138
|
-
epilog = _epilog_for(command_path)
|
|
143
|
+
"""Wrap add_parser so every parser gets aliyun-style formatting."""
|
|
139
144
|
kw.setdefault("formatter_class", AliyunStyleFormatter)
|
|
140
|
-
if epilog is not None and "epilog" not in kw:
|
|
141
|
-
kw["epilog"] = epilog
|
|
142
145
|
return parent_subparsers.add_parser(name, **kw)
|
|
143
146
|
|
|
144
147
|
|
|
@@ -159,13 +162,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
159
162
|
cli_version = get_version("maxc-cli")
|
|
160
163
|
except Exception:
|
|
161
164
|
from maxc_cli import __version__ as cli_version
|
|
165
|
+
cli_name = os.environ.get("MAXC_CLI_NAME", "").strip() or "maxc"
|
|
162
166
|
parser = argparse.ArgumentParser(
|
|
163
|
-
prog=
|
|
164
|
-
description="MaxCompute CLI — 给 Agent 调用的结构化工具层",
|
|
167
|
+
prog=cli_name,
|
|
165
168
|
formatter_class=AliyunStyleFormatter,
|
|
166
|
-
epilog=_epilog_for("__top__"),
|
|
167
169
|
)
|
|
168
|
-
parser.add_argument("-v", "--version", action="version", version=f"
|
|
170
|
+
parser.add_argument("-v", "--version", action="version", version=f"{cli_name} {cli_version}")
|
|
169
171
|
parser.add_argument("--config", help="Explicit path to a config file")
|
|
170
172
|
parser.add_argument(
|
|
171
173
|
"--format",
|
|
@@ -187,7 +189,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
187
189
|
|
|
188
190
|
# Top-level subparsers are NOT required: bare `maxc` is handled in run()
|
|
189
191
|
# (prints help when auth is configured, redirects to `auth login` otherwise).
|
|
190
|
-
|
|
192
|
+
# Explicit prog= prevents argparse from calling format_help() to derive it
|
|
193
|
+
# (which would bake our version header into child parser prog strings).
|
|
194
|
+
subparsers = parser.add_subparsers(dest="command_group", prog=cli_name)
|
|
191
195
|
|
|
192
196
|
query_parser = _make_parser(
|
|
193
197
|
subparsers,
|
|
@@ -197,13 +201,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
197
201
|
description=(
|
|
198
202
|
"Run a SQL query.\n"
|
|
199
203
|
"Usage:\n"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
+
f" {cli_name} query \"SELECT 1\" # default: run\n"
|
|
205
|
+
f" {cli_name} query run \"SELECT 1\" # explicit run\n"
|
|
206
|
+
f" {cli_name} query cost \"SELECT 1\" # estimate cost\n"
|
|
207
|
+
f" {cli_name} query explain \"SELECT 1\" # show plan\n"
|
|
204
208
|
"\n"
|
|
205
209
|
"Legacy usage (--mode is deprecated):\n"
|
|
206
|
-
"
|
|
210
|
+
f" {cli_name} query \"SELECT 1\" --mode cost"
|
|
207
211
|
),
|
|
208
212
|
formatter_class=AliyunRawTextFormatter,
|
|
209
213
|
)
|
|
@@ -1297,23 +1301,27 @@ def _handle_session_set(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
|
|
|
1297
1301
|
"""Set current project and/or schema for the session."""
|
|
1298
1302
|
project = args.project
|
|
1299
1303
|
schema = args.schema
|
|
1300
|
-
|
|
1304
|
+
|
|
1301
1305
|
if not project and not schema:
|
|
1302
1306
|
raise ValidationError("At least one of `--project` or `--schema` must be specified.")
|
|
1303
|
-
|
|
1304
|
-
envelope = app.session_set(
|
|
1307
|
+
|
|
1308
|
+
envelope = app.session_set(
|
|
1309
|
+
project=project,
|
|
1310
|
+
schema=schema,
|
|
1311
|
+
target_config_path=args.requested_config_path,
|
|
1312
|
+
)
|
|
1305
1313
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1306
1314
|
|
|
1307
1315
|
|
|
1308
1316
|
def _handle_session_show(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1309
1317
|
"""Show current session settings."""
|
|
1310
|
-
envelope = app.session_show()
|
|
1318
|
+
envelope = app.session_show(target_config_path=args.requested_config_path)
|
|
1311
1319
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1312
1320
|
|
|
1313
1321
|
|
|
1314
1322
|
def _handle_session_unset(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1315
1323
|
"""Clear session override."""
|
|
1316
|
-
envelope = app.session_unset()
|
|
1324
|
+
envelope = app.session_unset(target_config_path=args.requested_config_path)
|
|
1317
1325
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1318
1326
|
|
|
1319
1327
|
|
|
@@ -1447,23 +1455,13 @@ def _handle_agent_skill(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
|
|
|
1447
1455
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1448
1456
|
|
|
1449
1457
|
|
|
1450
|
-
def
|
|
1451
|
-
"""
|
|
1458
|
+
def _detect_cli_name() -> str:
|
|
1459
|
+
"""Read MAXC_CLI_NAME env var. Defaults to 'maxc'.
|
|
1452
1460
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
fall back to scanning argv[0] for `aliyun`. Defaults to `maxc`.
|
|
1461
|
+
The value is used directly as {{cli}} in SKILL templates.
|
|
1462
|
+
Example: MAXC_CLI_NAME='aliyun maxc' renders all commands as `aliyun maxc ...`.
|
|
1456
1463
|
"""
|
|
1457
|
-
|
|
1458
|
-
explicit = os.environ.get("MAXC_INVOCATION", "").strip().lower()
|
|
1459
|
-
if explicit in ("maxc", "aliyun-maxc"):
|
|
1460
|
-
return explicit
|
|
1461
|
-
if os.environ.get("ALIYUN_CLI_NAME") or os.environ.get("ALIYUN_CLI_VERSION"):
|
|
1462
|
-
return "aliyun-maxc"
|
|
1463
|
-
argv0 = (sys.argv[0] if sys.argv else "").lower()
|
|
1464
|
-
if "aliyun" in os.path.basename(argv0):
|
|
1465
|
-
return "aliyun-maxc"
|
|
1466
|
-
return "maxc"
|
|
1464
|
+
return os.environ.get("MAXC_CLI_NAME", "").strip() or "maxc"
|
|
1467
1465
|
|
|
1468
1466
|
|
|
1469
1467
|
def _resolve_dir_override(args: argparse.Namespace) -> Path | None:
|
|
@@ -1472,7 +1470,7 @@ def _resolve_dir_override(args: argparse.Namespace) -> Path | None:
|
|
|
1472
1470
|
|
|
1473
1471
|
|
|
1474
1472
|
def _handle_agent_skill_install(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1475
|
-
invocation = args.invocation or
|
|
1473
|
+
invocation = args.invocation or _detect_cli_name()
|
|
1476
1474
|
envelope = app.skill_install(
|
|
1477
1475
|
platform=args.platform,
|
|
1478
1476
|
invocation=invocation,
|
|
@@ -1483,7 +1481,7 @@ def _handle_agent_skill_install(app: MaxCApp, args: argparse.Namespace, stdout:
|
|
|
1483
1481
|
|
|
1484
1482
|
|
|
1485
1483
|
def _handle_agent_skill_update(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1486
|
-
invocation = args.invocation or
|
|
1484
|
+
invocation = args.invocation or _detect_cli_name()
|
|
1487
1485
|
# skill_update doesn't accept dir_override — `--all` iterates registry by
|
|
1488
1486
|
# default install_root, single-platform mode falls through to skill_install
|
|
1489
1487
|
# which would accept dir_override if we plumbed it in. Surface a validation
|
|
@@ -38,13 +38,11 @@ def strip_ansi(text: str) -> str:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class AliyunStyleFormatter(argparse.HelpFormatter):
|
|
41
|
-
"""Aliyun
|
|
41
|
+
"""Aliyun CLI style: version header, Commands, Flags (long first, no-space comma)."""
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
"""
|
|
43
|
+
_has_subparsers = False
|
|
45
44
|
|
|
46
45
|
_SECTION_REMAP = {
|
|
47
|
-
"positional arguments": "Commands",
|
|
48
46
|
"options": "Flags", # Python 3.10+
|
|
49
47
|
"optional arguments": "Flags", # Python 3.9
|
|
50
48
|
}
|
|
@@ -52,11 +50,18 @@ class AliyunStyleFormatter(argparse.HelpFormatter):
|
|
|
52
50
|
def start_section(self, heading):
|
|
53
51
|
if heading in self._SECTION_REMAP:
|
|
54
52
|
heading = self._SECTION_REMAP[heading]
|
|
53
|
+
# Rename "positional arguments" dynamically: "Commands" when a
|
|
54
|
+
# subparsers action exists, "Arguments" otherwise.
|
|
55
|
+
if heading == "positional arguments":
|
|
56
|
+
heading = "Commands" if self._has_subparsers else "Arguments"
|
|
55
57
|
super().start_section(heading)
|
|
56
58
|
|
|
57
59
|
def add_usage(self, usage, actions, groups, prefix=None):
|
|
58
60
|
if prefix is None:
|
|
59
61
|
prefix = "Usage:\n "
|
|
62
|
+
self._has_subparsers = any(
|
|
63
|
+
isinstance(a, argparse._SubParsersAction) for a in (actions or [])
|
|
64
|
+
)
|
|
60
65
|
super().add_usage(usage, actions, groups, prefix)
|
|
61
66
|
|
|
62
67
|
def _format_usage(self, usage, actions, groups, prefix):
|
|
@@ -83,49 +88,50 @@ class AliyunStyleFormatter(argparse.HelpFormatter):
|
|
|
83
88
|
line = " ".join(parts)
|
|
84
89
|
return f"{prefix}{line}\n\n"
|
|
85
90
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
text = super().format_help()
|
|
96
|
-
if not text.endswith("\n"):
|
|
97
|
-
text += "\n"
|
|
98
|
-
# Skip the footer on the top-level parser (matches aliyun behavior).
|
|
99
|
-
# Subparsers always render as ``"<prog> <group>..."`` with spaces; the
|
|
100
|
-
# root prog is bare (no spaces), so this check is durable across
|
|
101
|
-
# entry-point renames.
|
|
102
|
-
if " " not in self._prog:
|
|
103
|
-
return text
|
|
104
|
-
# Skip the footer when argparse calls format_help() internally just to
|
|
105
|
-
# derive a subparser's prog (``add_subparsers`` adds only the usage
|
|
106
|
-
# section then calls format_help().strip()). The root section contains
|
|
107
|
-
# exactly one usage item in that case; for a real --help invocation it
|
|
108
|
-
# contains usage + the action groups (Flags, Commands, ...).
|
|
109
|
-
if len(self._root_section.items) <= 1:
|
|
110
|
-
return text
|
|
111
|
-
text += f"\nUse `{self._prog} --help` for more information.\n"
|
|
112
|
-
return text
|
|
91
|
+
def _format_action_invocation(self, action):
|
|
92
|
+
if not action.option_strings:
|
|
93
|
+
# Positional with choices: show dest name, not {choice1,choice2,...}
|
|
94
|
+
if action.choices and not isinstance(action, argparse._SubParsersAction):
|
|
95
|
+
return action.dest
|
|
96
|
+
return super()._format_action_invocation(action)
|
|
97
|
+
# Long option first, short after, comma with no space
|
|
98
|
+
opts = sorted(action.option_strings, key=lambda s: (not s.startswith('--'), s))
|
|
99
|
+
return ','.join(opts)
|
|
113
100
|
|
|
114
101
|
def _format_action(self, action):
|
|
115
|
-
text = super()._format_action(action)
|
|
116
102
|
if isinstance(action, argparse._SubParsersAction):
|
|
103
|
+
# Skip the "{choices}" summary line, render only the sub-commands
|
|
104
|
+
parts = []
|
|
105
|
+
for sub_action in action._get_subactions():
|
|
106
|
+
parts.append(super()._format_action(sub_action))
|
|
107
|
+
text = ''.join(parts)
|
|
108
|
+
# Colorize command names
|
|
117
109
|
for choice in action.choices:
|
|
118
|
-
# Anchor to line start: argparse renders subcommand rows as
|
|
119
|
-
# ``" <choice> <help>"`` (leading whitespace varies with
|
|
120
|
-
# nesting). Plain ``str.replace`` would match the first
|
|
121
|
-
# occurrence anywhere in the rendered block (e.g. inside
|
|
122
|
-
# ``{login}`` brace lists or inside a help string).
|
|
123
110
|
text = re.sub(
|
|
124
111
|
rf"(?m)^(\s+){re.escape(choice)}(?=\s)",
|
|
125
112
|
rf"\1{cyan(choice)}",
|
|
126
113
|
text,
|
|
127
114
|
count=1,
|
|
128
115
|
)
|
|
116
|
+
return text
|
|
117
|
+
return super()._format_action(action)
|
|
118
|
+
|
|
119
|
+
def _fill_text(self, text, width, indent):
|
|
120
|
+
if "\n" in text:
|
|
121
|
+
return "".join(indent + line for line in text.splitlines(keepends=True))
|
|
122
|
+
return super()._fill_text(text, width, indent)
|
|
123
|
+
|
|
124
|
+
def format_help(self):
|
|
125
|
+
text = super().format_help()
|
|
126
|
+
if not text.endswith("\n"):
|
|
127
|
+
text += "\n"
|
|
128
|
+
# Inject version header at top for root parser's full help only.
|
|
129
|
+
# Guard on "Usage:" to skip injection when argparse uses format_help()
|
|
130
|
+
# for non-help purposes (e.g. --version action formatting).
|
|
131
|
+
if " " not in self._prog and "Usage:" in text:
|
|
132
|
+
from maxc_cli import __version__
|
|
133
|
+
header = f"MaxCompute CLI {__version__}\n\n"
|
|
134
|
+
text = header + text
|
|
129
135
|
return text
|
|
130
136
|
|
|
131
137
|
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: maxc-cli
|
|
3
3
|
description: Use when the task involves MaxCompute, ODPS, or {{cli}} — querying tables, viewing table schema, listing tables, searching metadata, executing SQL, checking partitions, sampling data, uploading or downloading CSV data, managing jobs, or generating MaxCompute SQL.
|
|
4
|
-
description_zh: 当用户需要查询 MaxCompute/ODPS 中的表、查看表结构、列出项目中的表、搜索元数据、执行 SQL、查看分区、预览数据、上传或下载 CSV 数据、跟踪任务或生成 MaxCompute SQL 时使用。适用于提到 ODPS、MaxCompute、maxc、数据仓库表查询、text2sql 等场景。
|
|
5
|
-
name_zh: MaxCompute数据查询
|
|
6
|
-
category: database
|
|
7
|
-
keywords: [MaxCompute, ODPS, maxc, 表, 查表, 查数据, SQL, 数据仓库, 元数据, 分区, odps sql, 阿里云, 上传, 下载, CSV, tunnel, text2sql, sql generation, SQL 生成]
|
|
8
|
-
requires: MaxCompute account with AK/SK or environment variables
|
|
9
|
-
entry_point: maxc
|
|
10
|
-
min_cli_version: "0.2.4"
|
|
11
4
|
---
|
|
12
5
|
|
|
13
6
|
# Use MaxC CLI
|
|
@@ -28,10 +28,9 @@ def test_resolve_unknown_raises():
|
|
|
28
28
|
ap.resolve("nonexistent")
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def test_claude_code_has_no_extra_files():
|
|
32
32
|
claude = ap.resolve("claude-code")
|
|
33
|
-
assert
|
|
34
|
-
for ef in claude.extra_files)
|
|
33
|
+
assert claude.extra_files == ()
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def test_install_root_matches_legacy_paths():
|
|
@@ -40,12 +39,12 @@ def test_install_root_matches_legacy_paths():
|
|
|
40
39
|
# and the new value must be justified in the PR description (it would break
|
|
41
40
|
# already-installed users).
|
|
42
41
|
expected = {
|
|
43
|
-
"claude-code": Path.home() / ".claude" / "
|
|
44
|
-
"cursor": Path.home() / ".cursor" / "skills" / "
|
|
45
|
-
"windsurf": Path.home() / ".windsurf" / "skills" / "
|
|
46
|
-
"qwen": Path.home() / ".qwen" / "skills" / "
|
|
47
|
-
"qoder": Path.home() / ".qoder" / "skills" / "
|
|
48
|
-
"qoderwork": Path.home() / ".qoderwork" / "skills" / "
|
|
42
|
+
"claude-code": Path.home() / ".claude" / "skills" / "maxc-cli",
|
|
43
|
+
"cursor": Path.home() / ".cursor" / "skills" / "maxc-cli",
|
|
44
|
+
"windsurf": Path.home() / ".windsurf" / "skills" / "maxc-cli",
|
|
45
|
+
"qwen": Path.home() / ".qwen" / "skills" / "maxc-cli",
|
|
46
|
+
"qoder": Path.home() / ".qoder" / "skills" / "maxc-cli",
|
|
47
|
+
"qoderwork": Path.home() / ".qoderwork" / "skills" / "maxc-cli",
|
|
49
48
|
}
|
|
50
49
|
for name, expected_path in expected.items():
|
|
51
50
|
assert ap.resolve(name).install_root == expected_path, name
|
|
@@ -61,7 +60,7 @@ def test_codex_install_root_respects_CODEX_HOME(monkeypatch, tmp_path):
|
|
|
61
60
|
importlib.reload(ap)
|
|
62
61
|
try:
|
|
63
62
|
assert ap.resolve("codex").install_root == (
|
|
64
|
-
tmp_path / "my-codex" / "skills" / "
|
|
63
|
+
tmp_path / "my-codex" / "skills" / "maxc-cli"
|
|
65
64
|
)
|
|
66
65
|
finally:
|
|
67
66
|
# Reload again after monkeypatch rollback so REGISTRY doesn't keep the
|
|
@@ -51,10 +51,10 @@ def test_skill_install_with_dir_override(app, tmp_path):
|
|
|
51
51
|
assert (custom / "SKILL.md").is_file()
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def
|
|
54
|
+
def test_skill_install_no_extra_files_for_claude_code(app, tmp_path):
|
|
55
55
|
env = app.skill_install(platform="claude-code", invocation="maxc")
|
|
56
56
|
install = Path(env.data["install_path"])
|
|
57
|
-
assert (install / ".claude-plugin"
|
|
57
|
+
assert not (install / ".claude-plugin").exists()
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def test_skill_install_force_overwrites(app, tmp_path):
|