feat: sap-cli skill v2.2.0 — upload + syntax-check commands
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: sap-cli
|
name: sap-cli
|
||||||
description: "Use when you need to operate SAP ABAP development objects — syncing code, querying metadata, managing transports, reading tables, or any task involving SAP ADT REST API. Trigger on: ABAP objects, SAP development, transport requests, DDIC types, SE11/SE16N/SE38/SE80 equivalents, 'sync to SAP', 'download from SAP', 'activate ABAP', 'read SAP table'. NOT for developing sap-cli itself."
|
description: "Use when you need to operate SAP ABAP development objects — syncing code, querying metadata, managing transports, reading tables, or any task involving SAP ADT REST API. Trigger on: ABAP objects, SAP development, transport requests, DDIC types, SE11/SE16N/SE38/SE80 equivalents, 'sync to SAP', 'download from SAP', 'activate ABAP', 'read SAP table'. NOT for developing sap-cli itself."
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
author: WuRangyu
|
author: WuRangyu
|
||||||
license: MIT
|
license: MIT
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
sap-cli — 基于 SAP ADT REST API 的 ABAP 开发对象管理工具
|
sap-cli — 基于 SAP ADT REST API 的 ABAP 开发对象管理工具
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2.1.0"
|
__version__ = "2.2.0"
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ from sapcli.commands import (
|
|||||||
cmd_read_table,
|
cmd_read_table,
|
||||||
cmd_run_program,
|
cmd_run_program,
|
||||||
cmd_activate,
|
cmd_activate,
|
||||||
|
cmd_upload,
|
||||||
|
cmd_syntax_check,
|
||||||
)
|
)
|
||||||
from sapcli.auth import cmd_auth_login, cmd_auth_logout, cmd_auth_status
|
from sapcli.auth import cmd_auth_login, cmd_auth_logout, cmd_auth_status
|
||||||
from sapcli.exceptions import SapCliError, ConfigError
|
from sapcli.exceptions import SapCliError, ConfigError
|
||||||
@@ -134,6 +136,8 @@ def main() -> None:
|
|||||||
"check": cmd_check,
|
"check": cmd_check,
|
||||||
"format": cmd_format,
|
"format": cmd_format,
|
||||||
"activate": cmd_activate,
|
"activate": cmd_activate,
|
||||||
|
"upload": cmd_upload,
|
||||||
|
"syntax-check": cmd_syntax_check,
|
||||||
"diff": cmd_diff,
|
"diff": cmd_diff,
|
||||||
"package": cmd_package,
|
"package": cmd_package,
|
||||||
"cds": cmd_cds,
|
"cds": cmd_cds,
|
||||||
|
|||||||
@@ -178,6 +178,18 @@ CDS View:
|
|||||||
act_parser.add_argument("--types", default=None, help="逗号分隔的多个对象类型(批量模式,与 --names 一一对应)")
|
act_parser.add_argument("--types", default=None, help="逗号分隔的多个对象类型(批量模式,与 --names 一一对应)")
|
||||||
act_parser.add_argument("--corr_nr", default=None, help="传输请求编号")
|
act_parser.add_argument("--corr_nr", default=None, help="传输请求编号")
|
||||||
|
|
||||||
|
# ── upload (仅上传源码,不检查不激活) ──
|
||||||
|
upload_parser = subparsers.add_parser("upload", help="上传源代码到 SAP(不检查不激活)")
|
||||||
|
upload_parser.add_argument("--name", required=True, help="对象名称 (function 类型用'组名/模块名')")
|
||||||
|
upload_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型")
|
||||||
|
upload_parser.add_argument("--path", required=True, help="本地源代码文件路径 (.abap)")
|
||||||
|
upload_parser.add_argument("--corr_nr", default=None, help="传输请求编号")
|
||||||
|
|
||||||
|
# ── syntax-check (远程对象语法检查,不上传不激活) ──
|
||||||
|
sc_parser = subparsers.add_parser("syntax-check", help="语法检查(远程对象,不上传不激活)")
|
||||||
|
sc_parser.add_argument("--name", required=True, help="对象名称")
|
||||||
|
sc_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型")
|
||||||
|
|
||||||
# ── diff (代码差异对比) ──
|
# ── diff (代码差异对比) ──
|
||||||
diff_parser = subparsers.add_parser("diff", help="本地 vs SAP 代码差异对比")
|
diff_parser = subparsers.add_parser("diff", help="本地 vs SAP 代码差异对比")
|
||||||
diff_parser.add_argument("--name", required=True, help="对象名称")
|
diff_parser.add_argument("--name", required=True, help="对象名称")
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ from sapcli.commands.program_run import (
|
|||||||
from sapcli.commands.activate import (
|
from sapcli.commands.activate import (
|
||||||
cmd_activate,
|
cmd_activate,
|
||||||
)
|
)
|
||||||
|
from sapcli.commands.upload import (
|
||||||
|
cmd_upload,
|
||||||
|
)
|
||||||
|
from sapcli.commands.syntax_check import (
|
||||||
|
cmd_syntax_check,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"cmd_create",
|
"cmd_create",
|
||||||
@@ -80,4 +86,6 @@ __all__ = [
|
|||||||
"cmd_read_table",
|
"cmd_read_table",
|
||||||
"cmd_run_program",
|
"cmd_run_program",
|
||||||
"cmd_activate",
|
"cmd_activate",
|
||||||
|
"cmd_upload",
|
||||||
|
"cmd_syntax_check",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""syntax-check 命令 — 对 SAP 远程对象执行语法检查(不上传不激活)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sapcli.client import ADTClient
|
||||||
|
from sapcli.exceptions import InvalidNameError, ObjectNotFoundError
|
||||||
|
from sapcli.types import get_type_config, parse_object_name
|
||||||
|
|
||||||
|
logger = logging.getLogger("sapcli.commands.syntax_check")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_syntax_check(args: argparse.Namespace, client: ADTClient) -> None:
|
||||||
|
"""对 SAP 远程对象执行语法检查(不上传代码、不激活)。
|
||||||
|
|
||||||
|
检查 SAP 上现有对象的源码(通常是 active 状态),用于验证数据库表
|
||||||
|
结构变更后,关联程序是否报语法错误。
|
||||||
|
"""
|
||||||
|
name: str = args.name
|
||||||
|
obj_type: str = args.type
|
||||||
|
|
||||||
|
parsed = parse_object_name(name, obj_type)
|
||||||
|
type_label = get_type_config(obj_type).label
|
||||||
|
|
||||||
|
if parsed.src_uri is None:
|
||||||
|
print(f"\n ✗ {type_label}没有源代码,不支持 syntax-check 操作")
|
||||||
|
print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件")
|
||||||
|
raise InvalidNameError(f"{type_label}没有源代码,不支持 syntax-check 操作")
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(" SAP ADT 语法检查(远程对象,不上传不激活)")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" 对象类型: {type_label}")
|
||||||
|
print(f" 对象名称: {parsed.display_name}")
|
||||||
|
|
||||||
|
print(f"\n → 检查对象是否存在...")
|
||||||
|
if not client.object_exists(parsed.obj_uri):
|
||||||
|
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||||
|
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||||
|
print(" ✓ 对象存在")
|
||||||
|
|
||||||
|
print(f"\n → 正在执行语法检查...")
|
||||||
|
ok, messages = client.syntax_check(name, parsed.obj_uri)
|
||||||
|
|
||||||
|
errors = [m for m in messages if m["type"] == "E"]
|
||||||
|
warnings = [m for m in messages if m["type"] == "W"]
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
print(f"\n ✓ 语法检查通过")
|
||||||
|
if warnings:
|
||||||
|
print(f" ({len(warnings)} 个警告)")
|
||||||
|
for w in warnings:
|
||||||
|
print(f" [警告] 行 {w['line']}: {w['text']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n ✗ 语法检查未通过! {len(errors)} 个错误, {len(warnings)} 个警告")
|
||||||
|
for e in errors:
|
||||||
|
print(f" [错误] 行 {e['line']}: {e['text']}")
|
||||||
|
for w in warnings:
|
||||||
|
print(f" [警告] 行 {w['line']}: {w['text']}")
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""upload 命令 — 仅上传源代码到 SAP(锁定→写入→解锁,不检查不激活)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from sapcli.client import ADTClient
|
||||||
|
from sapcli.commands.crud import _select_transport_request
|
||||||
|
from sapcli.exceptions import (
|
||||||
|
ConfigError,
|
||||||
|
InvalidNameError,
|
||||||
|
LockError,
|
||||||
|
ObjectNotFoundError,
|
||||||
|
)
|
||||||
|
from sapcli.types import get_type_config, parse_object_name
|
||||||
|
|
||||||
|
logger = logging.getLogger("sapcli.commands.upload")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None:
|
||||||
|
"""上传本地源代码到 SAP(仅锁定→写入→解锁,不检查不激活)。
|
||||||
|
|
||||||
|
与 sync 的区别:
|
||||||
|
- 不自动创建对象(对象必须已存在,否则报 ObjectNotFoundError)
|
||||||
|
- 不执行语法检查
|
||||||
|
- 不执行激活
|
||||||
|
|
||||||
|
适用于自动化测试场景:修改代码后先安全写入 SAP,避开 activate 的
|
||||||
|
锁定冲突,稍后再单独 activate。
|
||||||
|
"""
|
||||||
|
name: str = args.name
|
||||||
|
obj_type: str = args.type
|
||||||
|
file_path: str = args.path
|
||||||
|
corr_nr: str | None = getattr(args, "corr_nr", None)
|
||||||
|
|
||||||
|
parsed = parse_object_name(name, obj_type)
|
||||||
|
type_label = get_type_config(obj_type).label
|
||||||
|
|
||||||
|
if parsed.src_uri is None:
|
||||||
|
print(f"\n ✗ {type_label}没有源代码,不支持 upload 操作")
|
||||||
|
print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件")
|
||||||
|
raise InvalidNameError(f"{type_label}没有源代码,不支持 upload 操作")
|
||||||
|
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
print(f"\n ✗ 文件不存在: {file_path}")
|
||||||
|
raise ConfigError(f"文件不存在: {file_path}")
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(" SAP ADT 源代码上传(不检查不激活)")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" 对象类型: {type_label}")
|
||||||
|
print(f" 对象名称: {parsed.display_name}")
|
||||||
|
print(f" 本地文件: {file_path}")
|
||||||
|
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())} 行")
|
||||||
|
print(f" 流程: 检查 → 锁定 → 写入 → 解锁")
|
||||||
|
|
||||||
|
# 检查对象是否存在(upload 不自动创建,区别于 sync)
|
||||||
|
print(f"\n → 检查对象是否存在...")
|
||||||
|
if not client.object_exists(parsed.obj_uri):
|
||||||
|
print(f" ✗ 对象不存在: {parsed.display_name}")
|
||||||
|
print(f" upload 不会自动创建对象,请先用 create 或 sync 创建")
|
||||||
|
raise ObjectNotFoundError(parsed.display_name, obj_type)
|
||||||
|
print(" ✓ 对象存在")
|
||||||
|
|
||||||
|
# ── 锁定 → 写入 → 解锁(提取自 crud._sync_single,不含检查/激活)──
|
||||||
|
print("\n ── 锁定 → 写入 → 解锁 ──")
|
||||||
|
lock_handle: str
|
||||||
|
if corr_nr:
|
||||||
|
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
|
||||||
|
print(f" ✓ 锁定成功(传输请求: {corr_nr})")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
lock_handle, detected_corr_nr = client.lock(parsed.obj_uri)
|
||||||
|
if detected_corr_nr:
|
||||||
|
corr_nr = detected_corr_nr
|
||||||
|
print(f" ✓ 锁定成功(对象已绑定传输请求: {corr_nr})")
|
||||||
|
else:
|
||||||
|
print(f" ✓ 锁定成功(本地对象,无需传输请求)")
|
||||||
|
except LockError:
|
||||||
|
print(" ℹ 对象需要传输请求号")
|
||||||
|
corr_nr = _select_transport_request(client)
|
||||||
|
if not corr_nr:
|
||||||
|
print(" ✗ 锁定失败: 无法获取传输请求号")
|
||||||
|
raise LockError("锁定失败: 无法获取传输请求号", obj_uri=parsed.obj_uri)
|
||||||
|
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
|
||||||
|
print(f" ✓ 锁定成功(传输请求: {corr_nr})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.set_source(parsed.src_uri, source, lock_handle, corr_nr)
|
||||||
|
print(f" ✓ 源代码写入成功")
|
||||||
|
finally:
|
||||||
|
client.unlock(parsed.obj_uri, lock_handle)
|
||||||
|
print(f" ✓ 解锁成功")
|
||||||
|
|
||||||
|
print(f"\n {'=' * 60}")
|
||||||
|
print(f" ✓ {parsed.display_name} 源代码已上传")
|
||||||
|
print(f" 文件: {file_path}")
|
||||||
|
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())} 行")
|
||||||
|
print(f" 注意: 未执行语法检查和激活,请按需单独执行 syntax-check / activate")
|
||||||
|
print(f" {'=' * 60}")
|
||||||
Reference in New Issue
Block a user