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.
Files changed (95) hide show
  1. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/PKG-INFO +1 -1
  2. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/setup.py +1 -1
  3. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/agent_platforms.py +5 -5
  5. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/app.py +18 -6
  6. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/cli.py +23 -31
  7. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/help_format.py +43 -37
  8. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/SKILL.md +1 -8
  9. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  10. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_platforms.py +9 -10
  11. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_skill_commands.py +2 -2
  12. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_skill_commands_context.py +18 -18
  13. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_help_format.py +9 -9
  14. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/MANIFEST.in +0 -0
  15. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/README.md +0 -0
  16. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/pyproject.toml +0 -0
  17. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/scripts/pyinstaller_entry.py +0 -0
  18. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/scripts/regression_test.py +0 -0
  19. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/setup.cfg +0 -0
  20. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/__main__.py +0 -0
  21. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/_samples.py +0 -0
  22. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/audit.py +0 -0
  23. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/auth_providers.py +0 -0
  24. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/__init__.py +0 -0
  25. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/auth.py +0 -0
  26. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/catalog.py +0 -0
  27. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/data.py +0 -0
  28. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/job.py +0 -0
  29. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/meta.py +0 -0
  30. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/odps.py +0 -0
  31. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/backend/query.py +0 -0
  32. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/cache.py +0 -0
  33. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
  34. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/config.py +0 -0
  35. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/exceptions.py +0 -0
  36. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/helpers.py +0 -0
  37. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/masking.py +0 -0
  38. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/models.py +0 -0
  39. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/output.py +0 -0
  40. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/setting_parser.py +0 -0
  41. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  42. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  43. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  44. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  45. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  46. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  47. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  48. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
  49. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  50. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
  51. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
  52. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  53. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  54. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  55. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/store.py +0 -0
  56. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli/utils.py +0 -0
  57. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  58. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  59. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  60. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/requires.txt +0 -0
  61. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
  62. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_agent_hints_and_cli.py +0 -0
  63. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_data.py +0 -0
  64. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_data_serialization.py +0 -0
  65. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_backend_meta.py +0 -0
  66. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_build_release_script.py +0 -0
  67. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cache.py +0 -0
  68. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_catalog.py +0 -0
  69. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_catalog_bootstrap.py +0 -0
  70. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_arg_validation.py +0 -0
  71. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_mock.py +0 -0
  72. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  73. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_compat.py +0 -0
  74. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_e2e_smoke.py +0 -0
  75. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_envelope_shape.py +0 -0
  76. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_error_self_correction.py +0 -0
  77. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_error_translation.py +0 -0
  78. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_exit_codes.py +0 -0
  79. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_external_auth.py +0 -0
  80. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_flag_hoist.py +0 -0
  81. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_help_version_e2e.py +0 -0
  82. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_helpers.py +0 -0
  83. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_helpers_csv.py +0 -0
  84. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_integration.py +0 -0
  85. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_integration_real.py +0 -0
  86. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_job_improvements.py +0 -0
  87. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_masking.py +0 -0
  88. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
  89. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_phase1_improvements.py +0 -0
  90. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_pyinstaller_bundle.py +0 -0
  91. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_query_auto_promote.py +0 -0
  92. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_query_result_csv_fallback.py +0 -0
  93. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_setting_parser.py +0 -0
  94. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_skill_cli_consistency.py +0 -0
  95. {maxc_cli-0.3.3 → maxc_cli-0.4.0}/tests/test_skill_renderer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.3.3",
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",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.3.3"
5
+ __version__ = "0.4.0"
@@ -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" / "plugins" / "maxc-cli"
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
- / "maxcompute-cli-guidance"
86
+ / "maxc-cli"
87
87
  )
88
88
 
89
89
 
90
90
  def _simple_root(dotdir: str) -> Path:
91
- return Path.home() / dotdir / "skills" / "maxcompute-cli-guidance"
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=(ExtraFile(".claude-plugin/plugin.json", "render_claude_plugin"),),
121
- next_step_hint="Run /reload-plugins in Claude Code to activate",
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
- if invocation not in agent_platforms.INVOCATIONS:
3504
- raise ValidationError(
3505
- f"Unsupported invocation: {invocation}. "
3506
- f"Supported: {', '.join(agent_platforms.INVOCATIONS)}"
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 + Sample epilog."""
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="maxc",
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"maxc {cli_version}")
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
- subparsers = parser.add_subparsers(dest="command_group")
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
- " maxc query \"SELECT 1\" # default: run\n"
203
- " maxc query run \"SELECT 1\" # explicit run\n"
204
- " maxc query cost \"SELECT 1\" # estimate cost\n"
205
- " maxc query explain \"SELECT 1\" # show plan\n"
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
- " maxc query \"SELECT 1\" --mode cost"
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 _detect_invocation() -> str:
1457
- """Best-effort detection of the invocation form.
1458
+ def _detect_cli_name() -> str:
1459
+ """Read MAXC_CLI_NAME env var. Defaults to 'maxc'.
1458
1460
 
1459
- When the CLI is delegated to from `aliyun maxc ...` (via the Aliyun CLI
1460
- plugin bridge), `MAXC_INVOCATION` or `ALIYUN_CLI_NAME` is typically set;
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
- import os
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 _detect_invocation()
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 _detect_invocation()
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-style help: ``Usage:`` / ``Flags:`` / ``Commands:`` headings.
41
+ """Aliyun CLI style: version header, Commands, Flags (long first, no-space comma)."""
42
42
 
43
- Compact synopsis and Sample-epilog handling are added in Tasks 3-4.
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 _fill_text(self, text, width, indent):
87
- # Preserve sample/epilog formatting (newlines, leading indent) only for
88
- # pre-formatted multi-line text. Single-line strings fall through to
89
- # argparse's default ``textwrap.fill`` so long descriptions still wrap.
90
- if "\n" in text:
91
- return "".join(indent + line for line in text.splitlines(keepends=True))
92
- return super()._fill_text(text, width, indent)
93
-
94
- def format_help(self):
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: maxcompute-cli-guidance
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -28,10 +28,9 @@ def test_resolve_unknown_raises():
28
28
  ap.resolve("nonexistent")
29
29
 
30
30
 
31
- def test_claude_code_has_plugin_json_extra_file():
31
+ def test_claude_code_has_no_extra_files():
32
32
  claude = ap.resolve("claude-code")
33
- assert any(ef.relative_path == ".claude-plugin/plugin.json"
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" / "plugins" / "maxc-cli",
44
- "cursor": Path.home() / ".cursor" / "skills" / "maxcompute-cli-guidance",
45
- "windsurf": Path.home() / ".windsurf" / "skills" / "maxcompute-cli-guidance",
46
- "qwen": Path.home() / ".qwen" / "skills" / "maxcompute-cli-guidance",
47
- "qoder": Path.home() / ".qoder" / "skills" / "maxcompute-cli-guidance",
48
- "qoderwork": Path.home() / ".qoderwork" / "skills" / "maxcompute-cli-guidance",
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" / "maxcompute-cli-guidance"
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 test_skill_install_writes_extra_files_for_claude_code(app, tmp_path):
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" / "plugin.json").is_file()
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" / "plugins" / "maxc-cli",
226
- Path.home() / ".cursor" / "skills" / "maxcompute-cli-guidance",
227
- Path.home() / ".windsurf" / "skills" / "maxcompute-cli-guidance",
228
- Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxcompute-cli-guidance",
229
- Path.home() / ".qwen" / "skills" / "maxcompute-cli-guidance",
230
- Path.home() / ".qoder" / "skills" / "maxcompute-cli-guidance",
231
- Path.home() / ".qoderwork" / "skills" / "maxcompute-cli-guidance",
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" / "plugins" / "maxc-cli",
238
- Path.home() / ".cursor" / "skills" / "maxcompute-cli-guidance",
239
- Path.home() / ".windsurf" / "skills" / "maxcompute-cli-guidance",
240
- Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxcompute-cli-guidance",
241
- Path.home() / ".qwen" / "skills" / "maxcompute-cli-guidance",
242
- Path.home() / ".qoder" / "skills" / "maxcompute-cli-guidance",
243
- Path.home() / ".qoderwork" / "skills" / "maxcompute-cli-guidance",
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 "maxcompute-cli-guidance" in str(install_path)
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" / "plugins" / "maxc-cli"
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" / "plugins" / "maxc-cli"
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 "Commands:" in text
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 test_footer_hint_present_on_subparser(monkeypatch):
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 "Use `maxc auth --help` for more information." in text
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 test_footer_hint_absent_on_toplevel(monkeypatch):
101
- """The root `maxc` parser must not show a redundant 'Use `maxc --help`' footer."""
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 text.startswith("Usage:\n maxc")
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 "Sample:" in text
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