maxc-cli 0.3.0__tar.gz → 0.3.2__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.0 → maxc_cli-0.3.2}/PKG-INFO +5 -5
  2. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/README.md +4 -4
  3. maxc_cli-0.3.2/pyproject.toml +22 -0
  4. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/setup.py +1 -1
  5. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/__init__.py +1 -1
  6. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/__main__.py +0 -1
  7. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/_samples.py +13 -6
  8. maxc_cli-0.3.2/src/maxc_cli/agent_platforms.py +188 -0
  9. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/app.py +285 -191
  10. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/auth_providers.py +11 -13
  11. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/__init__.py +1 -2
  12. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/catalog.py +1 -1
  13. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/data.py +8 -10
  14. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/job.py +1 -1
  15. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/meta.py +8 -8
  16. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/odps.py +4 -5
  17. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/query.py +53 -18
  18. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/cache.py +1 -2
  19. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/cli.py +317 -116
  20. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/config.py +6 -6
  21. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/exceptions.py +10 -2
  22. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/help_format.py +0 -1
  23. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/helpers.py +113 -119
  24. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/masking.py +7 -8
  25. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/models.py +16 -9
  26. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/output.py +10 -11
  27. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/setting_parser.py +4 -4
  28. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/SKILL.md +17 -0
  29. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-auth.md +2 -0
  30. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-flow.md +2 -0
  31. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/command-patterns.md +10 -8
  32. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/json-output-format.md +2 -0
  33. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-select-guide.md +2 -0
  34. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +2 -0
  35. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +2 -0
  36. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/partition-guide.md +2 -0
  37. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/red-lines.md +2 -0
  38. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/setup-install.md +2 -0
  39. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-common-errors.md +2 -0
  40. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-query-patterns.md +2 -0
  41. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/text2sql-principles.md +2 -0
  42. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/utils.py +1 -2
  43. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/PKG-INFO +5 -5
  44. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/SOURCES.txt +6 -0
  45. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_agent_hints_and_cli.py +2 -2
  46. maxc_cli-0.3.2/tests/test_agent_platforms.py +95 -0
  47. maxc_cli-0.3.2/tests/test_agent_skill_commands.py +252 -0
  48. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_agent_skill_commands_context.py +30 -28
  49. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_backend_data.py +0 -1
  50. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_backend_data_serialization.py +26 -27
  51. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_backend_meta.py +0 -1
  52. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_cache.py +0 -1
  53. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_catalog.py +2 -3
  54. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_catalog_bootstrap.py +4 -3
  55. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_cli_mock.py +32 -18
  56. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_cli_query_parse_and_sanitize.py +9 -11
  57. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_envelope_shape.py +10 -11
  58. maxc_cli-0.3.2/tests/test_exit_codes.py +107 -0
  59. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_external_auth.py +138 -12
  60. maxc_cli-0.3.2/tests/test_flag_hoist.py +48 -0
  61. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_help_format.py +1 -1
  62. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_helpers.py +1 -1
  63. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_integration.py +4 -2
  64. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_integration_real.py +0 -1
  65. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_job_improvements.py +0 -1
  66. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_masking.py +1 -1
  67. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_meta_schema_and_partition_cols.py +16 -17
  68. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_phase1_improvements.py +22 -18
  69. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_query_auto_promote.py +4 -3
  70. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_setting_parser.py +43 -7
  71. maxc_cli-0.3.2/tests/test_skill_cli_consistency.py +75 -0
  72. maxc_cli-0.3.0/pyproject.toml +0 -7
  73. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/MANIFEST.in +0 -0
  74. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/scripts/pyinstaller_entry.py +0 -0
  75. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/scripts/regression_test.py +0 -0
  76. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/setup.cfg +0 -0
  77. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/audit.py +0 -0
  78. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/backend/auth.py +0 -0
  79. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/catalog_bootstrap.py +0 -0
  80. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  81. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli/store.py +0 -0
  82. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  83. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  84. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/requires.txt +0 -0
  85. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/top_level.txt +0 -0
  86. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_build_release_script.py +0 -0
  87. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_cli_arg_validation.py +0 -0
  88. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_compat.py +0 -0
  89. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_e2e_smoke.py +0 -0
  90. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_error_self_correction.py +0 -0
  91. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_error_translation.py +0 -0
  92. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_helpers_csv.py +1 -1
  93. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_pyinstaller_bundle.py +0 -0
  94. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/tests/test_query_result_csv_fallback.py +0 -0
  95. {maxc_cli-0.3.0 → maxc_cli-0.3.2}/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.0
3
+ Version: 0.3.2
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
@@ -59,7 +59,7 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
59
59
  | **session** | `set`, `show`, `unset` | 项目/Schema 切换 |
60
60
  | **diff** | `schema`, `partition`, `data` | 表结构/分区/数据对比 |
61
61
  | **cache** | `build`, `build-status`, `status`, `clear` | 元数据缓存管理 |
62
- | **agent** | `context`, `skill`, `install-skill` | Agent 集成与 SKILL 安装 |
62
+ | **agent** | `context`, `skill install`, `skill list` | Agent 集成与 SKILL 安装 |
63
63
 
64
64
  所有命令支持 `--json` 输出 Envelope v2.0 结构化响应。支持 `--format json|markdown|brief`(全局标志)。
65
65
 
@@ -69,16 +69,16 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
69
69
 
70
70
  SKILL HUB 安装 SKILL → Agent 读 SKILL.md → Agent 自己 `pip install maxc-cli`。
71
71
 
72
- ### 方式 2:install-skill(内网兜底)
72
+ ### 方式 2:agent skill install(内网兜底)
73
73
 
74
74
  先 pip install,再一键注册到 Agent 平台:
75
75
 
76
76
  ```bash
77
77
  pip install maxc-cli
78
- maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
78
+ maxc agent skill install claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
79
79
  ```
80
80
 
81
- `install-skill` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
81
+ `agent skill install` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
82
82
 
83
83
  ### preflight 检查
84
84
 
@@ -37,7 +37,7 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
37
37
  | **session** | `set`, `show`, `unset` | 项目/Schema 切换 |
38
38
  | **diff** | `schema`, `partition`, `data` | 表结构/分区/数据对比 |
39
39
  | **cache** | `build`, `build-status`, `status`, `clear` | 元数据缓存管理 |
40
- | **agent** | `context`, `skill`, `install-skill` | Agent 集成与 SKILL 安装 |
40
+ | **agent** | `context`, `skill install`, `skill list` | Agent 集成与 SKILL 安装 |
41
41
 
42
42
  所有命令支持 `--json` 输出 Envelope v2.0 结构化响应。支持 `--format json|markdown|brief`(全局标志)。
43
43
 
@@ -47,16 +47,16 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
47
47
 
48
48
  SKILL HUB 安装 SKILL → Agent 读 SKILL.md → Agent 自己 `pip install maxc-cli`。
49
49
 
50
- ### 方式 2:install-skill(内网兜底)
50
+ ### 方式 2:agent skill install(内网兜底)
51
51
 
52
52
  先 pip install,再一键注册到 Agent 平台:
53
53
 
54
54
  ```bash
55
55
  pip install maxc-cli
56
- maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
56
+ maxc agent skill install claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
57
57
  ```
58
58
 
59
- `install-skill` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
59
+ `agent skill install` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
60
60
 
61
61
  ### preflight 检查
62
62
 
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.pytest.ini_options]
6
+ pythonpath = ["src"]
7
+ testpaths = ["tests"]
8
+
9
+ [tool.ruff]
10
+ target-version = "py38"
11
+ line-length = 100
12
+
13
+ [tool.ruff.lint]
14
+ select = ["E", "F", "I", "UP"]
15
+ # E501 ignored for now: legacy codebase has 300+ long lines (mostly long SQL
16
+ # strings, dense parameter lists, and docstrings). Tighten in a later sweep
17
+ # once we've decided whether to wrap at the source or raise line-length.
18
+ ignore = ["E501"]
19
+
20
+ [tool.ruff.lint.per-file-ignores]
21
+ # Tests put `pytestmark = pytest.mark.unit` before imports on purpose.
22
+ "tests/*" = ["E402"]
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.3.0",
12
+ version="0.3.2",
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.0"
5
+ __version__ = "0.3.2"
@@ -1,6 +1,5 @@
1
1
 
2
2
  from .cli import main
3
3
 
4
-
5
4
  if __name__ == "__main__":
6
5
  raise SystemExit(main())
@@ -6,7 +6,6 @@ quoting style they'd actually type.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
9
-
10
9
  SAMPLES: dict[str, str] = {
11
10
  "__top__": (
12
11
  "maxc auth login\n"
@@ -147,14 +146,22 @@ SAMPLES: dict[str, str] = {
147
146
  "agent": (
148
147
  "maxc agent context\n"
149
148
  "maxc agent skill\n"
150
- "maxc agent install-skill claude-code"
149
+ "maxc agent skill install claude-code"
151
150
  ),
152
151
  "agent.context": "maxc agent context\nmaxc agent context --json",
153
152
  "agent.skill": "maxc agent skill\nmaxc agent skill --json",
154
- "agent.install-skill": (
155
- "maxc agent install-skill # defaults to claude-code\n"
156
- "maxc agent install-skill cursor"
157
- ),
153
+ "agent.skill.install": (
154
+ "maxc agent skill install claude-code\n"
155
+ "maxc agent skill install cursor --invocation aliyun-maxc"
156
+ ),
157
+ "agent.skill.update": (
158
+ "maxc agent skill update cursor\n"
159
+ "maxc agent skill update --all"
160
+ ),
161
+ "agent.skill.uninstall": "maxc agent skill uninstall cursor",
162
+ "agent.skill.list": "maxc agent skill list --json",
163
+ "agent.skill.diff": "maxc agent skill diff cursor --unified",
164
+ "agent.skill.path": "maxc agent skill path cursor\nmaxc agent skill path --source",
158
165
  # ── cache ──────────────────────────────────────────────────────────────
159
166
  "cache": (
160
167
  "maxc cache build --project my_proj\n"
@@ -0,0 +1,188 @@
1
+ """Agent platform registry — single source of truth for SKILL install targets.
2
+
3
+ Each Platform declares where SKILL.md (and optional extra files) live for a
4
+ specific agent product. The MaxCApp skill_install/update/uninstall/list/diff/path
5
+ methods all consult REGISTRY here, so adding a new platform = one entry below
6
+ (no code changes elsewhere).
7
+
8
+ CRITICAL: install_root values must stay byte-equivalent to the legacy
9
+ `_SKILL_PLATFORMS` table in `app.py:3375-3411` — see tests/test_agent_platforms.py
10
+ ::test_install_root_matches_legacy_paths.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Callable
18
+
19
+ RenderFn = Callable[[Path, str, str], None]
20
+ # (effective_install_root, cli, cli_module) -> None
21
+ # Callee mkdir + write_text directly; returns nothing.
22
+ # The first arg is the *effective* install root (after --dir override is applied
23
+ # by the caller), not platform.install_root.
24
+ # Args (cli, cli_module) are currently only consumed by SKILL.md template render;
25
+ # extra_files with fixed-literal output (e.g., plugin.json) may ignore them.
26
+ # Keep them in the signature so future extra_files using {{cli}} placeholders
27
+ # don't need a protocol bump.
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ExtraFile:
32
+ """Non-SKILL file co-installed with the skill (e.g., claude-plugin manifest).
33
+
34
+ `relative_path` is the discovery anchor for list/diff/uninstall — the
35
+ render_fn MUST write its content at `effective_install_root / relative_path`,
36
+ otherwise diff will report "file missing". Enforced by
37
+ test_render_claude_plugin_writes_declared_path.
38
+ """
39
+ relative_path: str
40
+ render_fn_name: str # name of a callable in this module matching RenderFn
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Platform:
45
+ name: str
46
+ install_root: Path
47
+ skill_subpath: str | None = None # SKILL contents location relative to install_root;
48
+ # None = write into effective_install_root directly
49
+ # (claude-code uses this — SKILL.md and .claude-plugin/
50
+ # are siblings).
51
+ requires_dir: bool = True
52
+ extra_files: tuple[ExtraFile, ...] = ()
53
+ aliases: tuple[str, ...] = ()
54
+ deprecated_aliases: tuple[str, ...] = ()
55
+ next_step_hint: str = ""
56
+
57
+
58
+ # Invocation map: cli front-end name → (cli, cli_module) substituted into
59
+ # SKILL templates. Migrated here from app.py:3363-3372 so the registry is the
60
+ # single source of truth.
61
+ INVOCATIONS: dict[str, dict[str, str]] = {
62
+ "maxc": {
63
+ "cli": "maxc",
64
+ # `python3 -m` (not `python`) matches the legacy _SKILL_INVOCATIONS at
65
+ # app.py:3363-3372 byte-for-byte — changing this re-renders SKILL.md for
66
+ # every existing install and triggers a phantom `agent skill diff`.
67
+ "cli_module": "python3 -m maxc_cli",
68
+ },
69
+ "aliyun-maxc": {
70
+ "cli": "aliyun maxc",
71
+ "cli_module": "aliyun maxc",
72
+ },
73
+ }
74
+
75
+
76
+ def _claude_root() -> Path:
77
+ return Path.home() / ".claude" / "plugins" / "maxc-cli"
78
+
79
+
80
+ def _codex_root() -> Path:
81
+ # Lazy — must re-read CODEX_HOME at call time, not module-import time,
82
+ # so the env var can be overridden in tests/CI.
83
+ return (
84
+ Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex")))
85
+ / "skills"
86
+ / "maxcompute-cli-guidance"
87
+ )
88
+
89
+
90
+ def _simple_root(dotdir: str) -> Path:
91
+ return Path.home() / dotdir / "skills" / "maxcompute-cli-guidance"
92
+
93
+
94
+ def render_claude_plugin(install_dir: Path, cli: str, cli_module: str) -> None:
95
+ """Write `.claude-plugin/plugin.json` under `install_dir`.
96
+
97
+ Byte-equivalent to legacy literal at `app.py:3493-3499`. Do NOT reformat
98
+ via json.dumps(indent=…) — that re-flows whitespace and causes
99
+ `agent skill diff` to report a phantom delta against pre-existing installs.
100
+ """
101
+ meta_dir = install_dir / ".claude-plugin"
102
+ meta_dir.mkdir(parents=True, exist_ok=True)
103
+ (meta_dir / "plugin.json").write_text(
104
+ '{\n "name": "maxc-cli",\n'
105
+ ' "description": "MaxCompute/ODPS CLI — query tables, view schema, '
106
+ 'search metadata, execute SQL, check partitions, sample data, '
107
+ 'track jobs. Install via: pip install maxc-cli",\n'
108
+ ' "author": { "name": "maxc-cli contributors" }\n}\n',
109
+ encoding="utf-8",
110
+ )
111
+
112
+
113
+ def _build_registry() -> tuple[Platform, ...]:
114
+ """Build REGISTRY lazily so tests can monkeypatch HOME/CODEX_HOME + reload."""
115
+ return (
116
+ Platform(
117
+ name="claude-code",
118
+ install_root=_claude_root(),
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",
122
+ ),
123
+ Platform(name="cursor", install_root=_simple_root(".cursor"),
124
+ next_step_hint="Restart Cursor to activate"),
125
+ Platform(name="windsurf", install_root=_simple_root(".windsurf"),
126
+ next_step_hint="Restart Windsurf to activate"),
127
+ Platform(name="codex", install_root=_codex_root(),
128
+ next_step_hint="Restart Codex to activate"),
129
+ Platform(name="qwen", install_root=_simple_root(".qwen"),
130
+ next_step_hint="Restart Qwen to activate"),
131
+ Platform(name="qoder", install_root=_simple_root(".qoder"),
132
+ next_step_hint="Restart Qoder to activate"),
133
+ Platform(name="qoderwork", install_root=_simple_root(".qoderwork"),
134
+ next_step_hint="Restart QoderWork to activate"),
135
+ )
136
+
137
+
138
+ REGISTRY: tuple[Platform, ...] = _build_registry()
139
+
140
+
141
+ def resolve(name_or_alias: str) -> Platform:
142
+ """Return the Platform matching name / alias / deprecated_alias.
143
+
144
+ Raises KeyError when no match. Caller is responsible for emitting a
145
+ stderr warning when the input matches `deprecated_aliases`.
146
+ """
147
+ for p in REGISTRY:
148
+ if name_or_alias == p.name:
149
+ return p
150
+ if name_or_alias in p.aliases:
151
+ return p
152
+ if name_or_alias in p.deprecated_aliases:
153
+ return p
154
+ raise KeyError(f"Unknown agent platform: {name_or_alias!r}")
155
+
156
+
157
+ def all_platforms() -> tuple[Platform, ...]:
158
+ return REGISTRY
159
+
160
+
161
+ def is_deprecated_alias(name: str) -> bool:
162
+ for p in REGISTRY:
163
+ if name in p.deprecated_aliases:
164
+ return True
165
+ return False
166
+
167
+
168
+ def effective_target(platform: Platform, dir_override: Path | None) -> Path:
169
+ """Resolve where SKILL content for `platform` should be written.
170
+
171
+ effective_target = (--dir override OR platform.install_root) / (skill_subpath or "")
172
+ """
173
+ root = dir_override if dir_override is not None else platform.install_root
174
+ if platform.skill_subpath:
175
+ return root / platform.skill_subpath
176
+ return root
177
+
178
+
179
+ def get_render_fn(name: str) -> RenderFn:
180
+ """Resolve a render_fn name (declared in ExtraFile) into the actual callable.
181
+
182
+ Kept here so caller can `getattr(agent_platforms, name)` without exposing the
183
+ module-internal naming convention to MaxCApp.
184
+ """
185
+ fn = globals().get(name)
186
+ if not callable(fn):
187
+ raise KeyError(f"render_fn not found in agent_platforms: {name!r}")
188
+ return fn # type: ignore[return-value]