maxc-cli 0.3.3__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.3 → maxc_cli-0.4.0}/PKG-INFO +1 -1
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/setup.py +1 -1
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/agent_platforms.py +5 -5
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/app.py +18 -6
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/cli.py +23 -31
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/help_format.py +43 -37
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/SKILL.md +1 -8
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_platforms.py +9 -10
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_skill_commands.py +2 -2
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_skill_commands_context.py +18 -18
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_help_format.py +9 -9
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/MANIFEST.in +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/README.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/pyproject.toml +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/scripts/regression_test.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/setup.cfg +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cache.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_catalog.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_compat.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_helpers.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_integration.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_masking.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.3.3 → 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"),
|
|
@@ -3491,6 +3491,16 @@ class MaxCApp:
|
|
|
3491
3491
|
|
|
3492
3492
|
return sorted(files_copied)
|
|
3493
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
|
+
|
|
3494
3504
|
def skill_install(
|
|
3495
3505
|
self,
|
|
3496
3506
|
*,
|
|
@@ -3500,14 +3510,16 @@ class MaxCApp:
|
|
|
3500
3510
|
force: 'bool' = False,
|
|
3501
3511
|
) -> 'Envelope':
|
|
3502
3512
|
from . import agent_platforms
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
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}
|
|
3508
3519
|
platform_spec, target = self._resolve_skill_target(platform, dir_override)
|
|
3509
|
-
invocation_map = agent_platforms.INVOCATIONS[invocation]
|
|
3510
3520
|
skills_src = self._locate_skills_source()
|
|
3521
|
+
if dir_override is None:
|
|
3522
|
+
self._cleanup_legacy_skill_dir(target)
|
|
3511
3523
|
version_marker = f"{__version__}+{invocation}"
|
|
3512
3524
|
marker_path = target / ".maxc-skill-version"
|
|
3513
3525
|
if not force and marker_path.is_file() and marker_path.read_text().strip() == version_marker:
|
|
@@ -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
|
|
|
@@ -136,11 +140,8 @@ def _hoist_global_flags(argv: list[str]) -> list[str]:
|
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
def _make_parser(parent_subparsers, name, command_path, **kw):
|
|
139
|
-
"""Wrap add_parser so every parser gets aliyun-style formatting
|
|
140
|
-
epilog = _epilog_for(command_path)
|
|
143
|
+
"""Wrap add_parser so every parser gets aliyun-style formatting."""
|
|
141
144
|
kw.setdefault("formatter_class", AliyunStyleFormatter)
|
|
142
|
-
if epilog is not None and "epilog" not in kw:
|
|
143
|
-
kw["epilog"] = epilog
|
|
144
145
|
return parent_subparsers.add_parser(name, **kw)
|
|
145
146
|
|
|
146
147
|
|
|
@@ -161,13 +162,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
161
162
|
cli_version = get_version("maxc-cli")
|
|
162
163
|
except Exception:
|
|
163
164
|
from maxc_cli import __version__ as cli_version
|
|
165
|
+
cli_name = os.environ.get("MAXC_CLI_NAME", "").strip() or "maxc"
|
|
164
166
|
parser = argparse.ArgumentParser(
|
|
165
|
-
prog=
|
|
166
|
-
description="MaxCompute CLI — 给 Agent 调用的结构化工具层",
|
|
167
|
+
prog=cli_name,
|
|
167
168
|
formatter_class=AliyunStyleFormatter,
|
|
168
|
-
epilog=_epilog_for("__top__"),
|
|
169
169
|
)
|
|
170
|
-
parser.add_argument("-v", "--version", action="version", version=f"
|
|
170
|
+
parser.add_argument("-v", "--version", action="version", version=f"{cli_name} {cli_version}")
|
|
171
171
|
parser.add_argument("--config", help="Explicit path to a config file")
|
|
172
172
|
parser.add_argument(
|
|
173
173
|
"--format",
|
|
@@ -189,7 +189,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
189
189
|
|
|
190
190
|
# Top-level subparsers are NOT required: bare `maxc` is handled in run()
|
|
191
191
|
# (prints help when auth is configured, redirects to `auth login` otherwise).
|
|
192
|
-
|
|
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)
|
|
193
195
|
|
|
194
196
|
query_parser = _make_parser(
|
|
195
197
|
subparsers,
|
|
@@ -199,13 +201,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
199
201
|
description=(
|
|
200
202
|
"Run a SQL query.\n"
|
|
201
203
|
"Usage:\n"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
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"
|
|
206
208
|
"\n"
|
|
207
209
|
"Legacy usage (--mode is deprecated):\n"
|
|
208
|
-
"
|
|
210
|
+
f" {cli_name} query \"SELECT 1\" --mode cost"
|
|
209
211
|
),
|
|
210
212
|
formatter_class=AliyunRawTextFormatter,
|
|
211
213
|
)
|
|
@@ -1453,23 +1455,13 @@ def _handle_agent_skill(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
|
|
|
1453
1455
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1454
1456
|
|
|
1455
1457
|
|
|
1456
|
-
def
|
|
1457
|
-
"""
|
|
1458
|
+
def _detect_cli_name() -> str:
|
|
1459
|
+
"""Read MAXC_CLI_NAME env var. Defaults to 'maxc'.
|
|
1458
1460
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
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 ...`.
|
|
1462
1463
|
"""
|
|
1463
|
-
|
|
1464
|
-
explicit = os.environ.get("MAXC_INVOCATION", "").strip().lower()
|
|
1465
|
-
if explicit in ("maxc", "aliyun-maxc"):
|
|
1466
|
-
return explicit
|
|
1467
|
-
if os.environ.get("ALIYUN_CLI_NAME") or os.environ.get("ALIYUN_CLI_VERSION"):
|
|
1468
|
-
return "aliyun-maxc"
|
|
1469
|
-
argv0 = (sys.argv[0] if sys.argv else "").lower()
|
|
1470
|
-
if "aliyun" in os.path.basename(argv0):
|
|
1471
|
-
return "aliyun-maxc"
|
|
1472
|
-
return "maxc"
|
|
1464
|
+
return os.environ.get("MAXC_CLI_NAME", "").strip() or "maxc"
|
|
1473
1465
|
|
|
1474
1466
|
|
|
1475
1467
|
def _resolve_dir_override(args: argparse.Namespace) -> Path | None:
|
|
@@ -1478,7 +1470,7 @@ def _resolve_dir_override(args: argparse.Namespace) -> Path | None:
|
|
|
1478
1470
|
|
|
1479
1471
|
|
|
1480
1472
|
def _handle_agent_skill_install(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1481
|
-
invocation = args.invocation or
|
|
1473
|
+
invocation = args.invocation or _detect_cli_name()
|
|
1482
1474
|
envelope = app.skill_install(
|
|
1483
1475
|
platform=args.platform,
|
|
1484
1476
|
invocation=invocation,
|
|
@@ -1489,7 +1481,7 @@ def _handle_agent_skill_install(app: MaxCApp, args: argparse.Namespace, stdout:
|
|
|
1489
1481
|
|
|
1490
1482
|
|
|
1491
1483
|
def _handle_agent_skill_update(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1492
|
-
invocation = args.invocation or
|
|
1484
|
+
invocation = args.invocation or _detect_cli_name()
|
|
1493
1485
|
# skill_update doesn't accept dir_override — `--all` iterates registry by
|
|
1494
1486
|
# default install_root, single-platform mode falls through to skill_install
|
|
1495
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):
|
|
@@ -222,25 +222,25 @@ class TestAgentInstallSkill:
|
|
|
222
222
|
"""Remove skill install dirs before each test to avoid stale version files."""
|
|
223
223
|
import shutil
|
|
224
224
|
for d in [
|
|
225
|
-
Path.home() / ".claude" / "
|
|
226
|
-
Path.home() / ".cursor" / "skills" / "
|
|
227
|
-
Path.home() / ".windsurf" / "skills" / "
|
|
228
|
-
Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "
|
|
229
|
-
Path.home() / ".qwen" / "skills" / "
|
|
230
|
-
Path.home() / ".qoder" / "skills" / "
|
|
231
|
-
Path.home() / ".qoderwork" / "skills" / "
|
|
225
|
+
Path.home() / ".claude" / "skills" / "maxc-cli",
|
|
226
|
+
Path.home() / ".cursor" / "skills" / "maxc-cli",
|
|
227
|
+
Path.home() / ".windsurf" / "skills" / "maxc-cli",
|
|
228
|
+
Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxc-cli",
|
|
229
|
+
Path.home() / ".qwen" / "skills" / "maxc-cli",
|
|
230
|
+
Path.home() / ".qoder" / "skills" / "maxc-cli",
|
|
231
|
+
Path.home() / ".qoderwork" / "skills" / "maxc-cli",
|
|
232
232
|
]:
|
|
233
233
|
if d.exists():
|
|
234
234
|
shutil.rmtree(str(d))
|
|
235
235
|
yield
|
|
236
236
|
for d in [
|
|
237
|
-
Path.home() / ".claude" / "
|
|
238
|
-
Path.home() / ".cursor" / "skills" / "
|
|
239
|
-
Path.home() / ".windsurf" / "skills" / "
|
|
240
|
-
Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "
|
|
241
|
-
Path.home() / ".qwen" / "skills" / "
|
|
242
|
-
Path.home() / ".qoder" / "skills" / "
|
|
243
|
-
Path.home() / ".qoderwork" / "skills" / "
|
|
237
|
+
Path.home() / ".claude" / "skills" / "maxc-cli",
|
|
238
|
+
Path.home() / ".cursor" / "skills" / "maxc-cli",
|
|
239
|
+
Path.home() / ".windsurf" / "skills" / "maxc-cli",
|
|
240
|
+
Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxc-cli",
|
|
241
|
+
Path.home() / ".qwen" / "skills" / "maxc-cli",
|
|
242
|
+
Path.home() / ".qoder" / "skills" / "maxc-cli",
|
|
243
|
+
Path.home() / ".qoderwork" / "skills" / "maxc-cli",
|
|
244
244
|
]:
|
|
245
245
|
if d.exists():
|
|
246
246
|
shutil.rmtree(str(d))
|
|
@@ -253,9 +253,9 @@ class TestAgentInstallSkill:
|
|
|
253
253
|
assert data["platform"] == "claude-code"
|
|
254
254
|
assert data["upgraded"] is True
|
|
255
255
|
install_path = Path(data["install_path"])
|
|
256
|
-
assert (install_path / ".claude-plugin" / "plugin.json").is_file()
|
|
257
256
|
assert (install_path / "SKILL.md").is_file()
|
|
258
257
|
assert (install_path / "references").is_dir()
|
|
258
|
+
assert not (install_path / ".claude-plugin").exists()
|
|
259
259
|
|
|
260
260
|
def test_install_skill_cursor(self, tmp_path):
|
|
261
261
|
config = _make_config(tmp_path)
|
|
@@ -265,7 +265,7 @@ class TestAgentInstallSkill:
|
|
|
265
265
|
assert data["platform"] == "cursor"
|
|
266
266
|
assert data["upgraded"] is True
|
|
267
267
|
install_path = Path(data["install_path"])
|
|
268
|
-
assert "
|
|
268
|
+
assert "maxc-cli" in str(install_path)
|
|
269
269
|
assert (install_path / "SKILL.md").is_file()
|
|
270
270
|
assert not (install_path / ".claude-plugin").exists()
|
|
271
271
|
|
|
@@ -351,7 +351,7 @@ class TestAgentInstallSkill:
|
|
|
351
351
|
"""If version marker differs, files should be overwritten."""
|
|
352
352
|
config = _make_config(tmp_path)
|
|
353
353
|
_run_cmd(config, ["agent", "skill", "install", "claude-code", "--json"])
|
|
354
|
-
install_path = Path.home() / ".claude" / "
|
|
354
|
+
install_path = Path.home() / ".claude" / "skills" / "maxc-cli"
|
|
355
355
|
(install_path / ".maxc-skill-version").write_text("0.0.0")
|
|
356
356
|
_, payload, _ = _run_cmd(config, ["agent", "skill", "install", "claude-code", "--json"])
|
|
357
357
|
assert payload["data"]["upgraded"] is True
|
|
@@ -360,7 +360,7 @@ class TestAgentInstallSkill:
|
|
|
360
360
|
def test_install_skill_version_file_created(self, tmp_path):
|
|
361
361
|
config = _make_config(tmp_path)
|
|
362
362
|
_run_cmd(config, ["agent", "skill", "install", "claude-code", "--json"])
|
|
363
|
-
install_path = Path.home() / ".claude" / "
|
|
363
|
+
install_path = Path.home() / ".claude" / "skills" / "maxc-cli"
|
|
364
364
|
version_file = install_path / ".maxc-skill-version"
|
|
365
365
|
assert version_file.is_file()
|
|
366
366
|
from maxc_cli import __version__
|
|
@@ -35,7 +35,7 @@ def test_section_headings_remapped():
|
|
|
35
35
|
assert "Flags:" in text
|
|
36
36
|
assert "options:" not in text
|
|
37
37
|
assert "optional arguments:" not in text
|
|
38
|
-
assert "
|
|
38
|
+
assert "Arguments:" in text
|
|
39
39
|
assert "positional arguments:" not in text
|
|
40
40
|
|
|
41
41
|
|
|
@@ -81,11 +81,12 @@ def test_epilog_rendered_as_sample_section():
|
|
|
81
81
|
assert ' maxc query "SELECT 1"' in text
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def
|
|
84
|
+
def test_no_footer_on_subparser(monkeypatch):
|
|
85
|
+
"""Aliyun style: no footer hint text."""
|
|
85
86
|
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
|
|
86
87
|
p = argparse.ArgumentParser(prog="maxc auth", formatter_class=AliyunStyleFormatter)
|
|
87
88
|
text = p.format_help()
|
|
88
|
-
assert "
|
|
89
|
+
assert "for more information." not in text
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
def test_subcommand_names_colored_when_tty(monkeypatch):
|
|
@@ -97,8 +98,8 @@ def test_subcommand_names_colored_when_tty(monkeypatch):
|
|
|
97
98
|
assert "\033[36mlogin\033[0m" in text
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
def
|
|
101
|
-
"""The root `maxc` parser must not show a
|
|
101
|
+
def test_no_footer_on_toplevel(monkeypatch):
|
|
102
|
+
"""The root `maxc` parser must not show a footer hint."""
|
|
102
103
|
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
|
|
103
104
|
p = argparse.ArgumentParser(prog="maxc", formatter_class=AliyunStyleFormatter)
|
|
104
105
|
text = p.format_help()
|
|
@@ -144,10 +145,10 @@ def test_top_level_maxc_help_uses_aliyun_style(monkeypatch):
|
|
|
144
145
|
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
|
|
145
146
|
from maxc_cli.cli import build_parser
|
|
146
147
|
text = build_parser().format_help()
|
|
147
|
-
assert
|
|
148
|
+
assert "MaxCompute CLI" in text
|
|
149
|
+
assert "Usage:\n maxc" in text
|
|
148
150
|
assert "Flags:" in text
|
|
149
151
|
assert "Commands:" in text
|
|
150
|
-
assert "Sample:" in text
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
def test_auth_login_help_uses_aliyun_style(monkeypatch):
|
|
@@ -162,5 +163,4 @@ def test_auth_login_help_uses_aliyun_style(monkeypatch):
|
|
|
162
163
|
text = login.format_help()
|
|
163
164
|
assert "Usage:" in text
|
|
164
165
|
assert "Flags:" in text
|
|
165
|
-
assert "
|
|
166
|
-
assert "Use `maxc auth login --help`" in text
|
|
166
|
+
assert "for more information." not in text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|