@qingflow-tech/qingflow-app-user-mcp 1.0.41 → 1.0.43
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.
- package/README.md +2 -4
- package/docs/local-agent-install.md +4 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +4 -4
- package/skills/qingflow-mcp-setup/SKILL.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +1 -1
- package/skills/qingflow-record-delete/SKILL.md +1 -1
- package/skills/qingflow-record-import/SKILL.md +1 -1
- package/skills/qingflow-record-insert/SKILL.md +1 -1
- package/skills/qingflow-record-update/SKILL.md +1 -1
- package/skills/qingflow-task-ops/SKILL.md +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +102 -2
- package/src/qingflow_mcp/cli/commands/builder.py +80 -6
- package/src/qingflow_mcp/response_trim.py +5 -4
- package/skills/qingflow-app-builder/SKILL.md +0 -290
- package/skills/qingflow-app-builder/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -59
- package/skills/qingflow-app-builder/references/create-app.md +0 -162
- package/skills/qingflow-app-builder/references/environments.md +0 -63
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +0 -123
- package/skills/qingflow-app-builder/references/gotchas.md +0 -113
- package/skills/qingflow-app-builder/references/match-rules.md +0 -129
- package/skills/qingflow-app-builder/references/public-surface-sync.md +0 -75
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -47
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -62
- package/skills/qingflow-app-builder/references/tool-selection.md +0 -106
- package/skills/qingflow-app-builder/references/update-flow.md +0 -158
- package/skills/qingflow-app-builder/references/update-layout.md +0 -68
- package/skills/qingflow-app-builder/references/update-schema.md +0 -75
- package/skills/qingflow-app-builder/references/update-views.md +0 -286
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +0 -139
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +0 -66
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +0 -77
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.43
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.43 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
|
@@ -24,8 +24,6 @@ Bundled skills:
|
|
|
24
24
|
|
|
25
25
|
- `skills/qingflow-app-user`
|
|
26
26
|
- `skills/qingflow-mcp-setup`
|
|
27
|
-
- `skills/qingflow-app-builder`
|
|
28
|
-
- `skills/qingflow-app-builder-code-integrations`
|
|
29
27
|
- `skills/qingflow-record-insert`
|
|
30
28
|
- `skills/qingflow-record-update`
|
|
31
29
|
- `skills/qingflow-record-delete`
|
|
@@ -128,7 +128,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
128
128
|
如果通过一次性 `npx -p <package>` 执行安装,请加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
|
-
npx -y -p @
|
|
131
|
+
npx -y -p @qingflow-tech/qingflow-cli qingflow-skills install --agent codex --scope user --copy
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
也可以挂载到项目级 agent 目录:
|
|
@@ -268,7 +268,7 @@ qingflow-app-builder-mcp-skills list
|
|
|
268
268
|
"command": "npx",
|
|
269
269
|
"args": [
|
|
270
270
|
"-y",
|
|
271
|
-
"@
|
|
271
|
+
"@qingflow-tech/qingflow-app-user-mcp"
|
|
272
272
|
],
|
|
273
273
|
"env": {
|
|
274
274
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
@@ -281,7 +281,7 @@ qingflow-app-builder-mcp-skills list
|
|
|
281
281
|
"command": "npx",
|
|
282
282
|
"args": [
|
|
283
283
|
"-y",
|
|
284
|
-
"@
|
|
284
|
+
"@qingflow-tech/qingflow-app-builder-mcp"
|
|
285
285
|
],
|
|
286
286
|
"env": {
|
|
287
287
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
@@ -323,7 +323,7 @@ npm install
|
|
|
323
323
|
|
|
324
324
|
如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
|
|
325
325
|
|
|
326
|
-
1. 不要混用不同版本的 `@
|
|
326
|
+
1. 不要混用不同版本的 `@qingflow-tech/qingflow-cli`、`@qingflow-tech/qingflow-app-user-mcp`、`@qingflow-tech/qingflow-app-builder-mcp`
|
|
327
327
|
2. 删除安装目录下的 `.npm-python`
|
|
328
328
|
3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
|
|
329
329
|
4. 再启动 MCP 客户端
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App User
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -41,7 +41,7 @@ Route to exactly one of these specialized paths:
|
|
|
41
41
|
Switch to [$qingflow-mcp-setup](../qingflow-mcp-setup/SKILL.md)
|
|
42
42
|
|
|
43
43
|
8. App / view / workflow / chart / portal / package configuration
|
|
44
|
-
|
|
44
|
+
This is outside the App User MCP tool surface. Use the separate `qingflow-app-builder` skill/MCP package when that builder surface is installed; otherwise ask the user to enable the builder MCP.
|
|
45
45
|
|
|
46
46
|
## Routing Rules
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ Route to exactly one of these specialized paths:
|
|
|
52
52
|
- If the task is about deleting records directly, switch to `$qingflow-record-delete`
|
|
53
53
|
- If the task is about import templates, import capability discovery, import-file verification, authorized local file repair, import execution, or import status, switch to `$qingflow-record-import`
|
|
54
54
|
- If the task is about todo discovery, task context, approval actions, rollback or transfer, associated report review, or workflow log review, switch to `$qingflow-task-ops`
|
|
55
|
-
- If the task is about package, app, field, layout, workflow, view, chart, portal, visibility, icon, or app base configuration,
|
|
55
|
+
- If the task is about package, app, field, layout, workflow, view, chart, portal, visibility, icon, or app base configuration, do not continue with App User MCP tools. Use the separate `qingflow-app-builder` skill/MCP package when available, or ask the user to enable the builder MCP.
|
|
56
56
|
- If the task involves member, department, or relation fields and the user only has natural names/titles, keep the same route; direct write now supports backend-native auto resolution and may return `needs_confirmation` with candidates instead of failing blind
|
|
57
57
|
- For member/department field ambiguity, keep the record insert/update route and use `record_member_candidates` / `record_department_candidates`; do not switch to `directory_*`, builder member search, external-contact lookup, or contact-directory management queries. App User MCP only exposes `directory_search` for member-visible keyword search, not directory tree/list management.
|
|
58
58
|
- If the task involves linked visibility, upstream/downstream field dependencies, reference-driven auto fill, or formula-driven defaulting, keep the same insert/update route and read field-level `linkage` from the schema before composing payloads
|
|
@@ -89,4 +89,4 @@ Route to exactly one of these specialized paths:
|
|
|
89
89
|
- Record import: [$qingflow-record-import](../qingflow-record-import/SKILL.md)
|
|
90
90
|
- Task workflow operations: [$qingflow-task-ops](../qingflow-task-ops/SKILL.md)
|
|
91
91
|
- Dedicated analysis workflow: [$qingflow-record-analysis](../qingflow-record-analysis/SKILL.md)
|
|
92
|
-
- Builder workflow
|
|
92
|
+
- Builder workflow requires the separate `qingflow-app-builder` skill/MCP package.
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow MCP Setup
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Analysis
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
Use this skill only for final statistical conclusions: counts, distributions, ratios, averages, rankings, trends, comparisons, and analysis reports.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Delete
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Import
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# Qingflow Record Insert
|
|
10
10
|
|
|
11
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
11
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
12
12
|
|
|
13
13
|
## Default Path
|
|
14
14
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Update
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Task Ops
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -650,6 +650,42 @@ class AiBuilderFacade:
|
|
|
650
650
|
normalized_args=normalized_args,
|
|
651
651
|
)
|
|
652
652
|
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
653
|
+
prior_package_write_executed = bool(
|
|
654
|
+
created
|
|
655
|
+
or (
|
|
656
|
+
metadata_requested
|
|
657
|
+
and isinstance(update_result, dict)
|
|
658
|
+
and update_result.get("status") in {"success", "partial_success"}
|
|
659
|
+
and not bool(update_result.get("noop"))
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
if prior_package_write_executed:
|
|
663
|
+
layout_details = layout_result.get("details") if isinstance(layout_result.get("details"), dict) else {}
|
|
664
|
+
partial = _post_write_readback_pending_result(
|
|
665
|
+
error_code=str(layout_result.get("error_code") or "PACKAGE_APPLY_PARTIAL"),
|
|
666
|
+
message="created or updated package, but package layout apply failed; read package before retrying",
|
|
667
|
+
normalized_args=normalized_args,
|
|
668
|
+
details={
|
|
669
|
+
"package_id": effective_package_id,
|
|
670
|
+
"layout_error_code": layout_result.get("error_code"),
|
|
671
|
+
"layout_result": layout_result,
|
|
672
|
+
**(
|
|
673
|
+
{"layout_write_error": layout_details.get("write_error")}
|
|
674
|
+
if isinstance(layout_details.get("write_error"), dict)
|
|
675
|
+
else {}
|
|
676
|
+
),
|
|
677
|
+
},
|
|
678
|
+
suggested_next_call={
|
|
679
|
+
"tool_name": "package_get",
|
|
680
|
+
"arguments": {"profile": profile, "package_id": effective_package_id},
|
|
681
|
+
},
|
|
682
|
+
request_id=layout_result.get("request_id") if isinstance(layout_result.get("request_id"), str) else None,
|
|
683
|
+
backend_code=layout_result.get("backend_code"),
|
|
684
|
+
http_status=layout_result.get("http_status") if isinstance(layout_result.get("http_status"), int) else None,
|
|
685
|
+
)
|
|
686
|
+
partial["package_id"] = effective_package_id
|
|
687
|
+
partial["layout_failed"] = True
|
|
688
|
+
return _apply_permission_outcomes(partial, *permission_outcomes)
|
|
653
689
|
return _apply_permission_outcomes(layout_result, *permission_outcomes)
|
|
654
690
|
|
|
655
691
|
write_executed = bool(
|
|
@@ -13881,7 +13917,7 @@ def _failed_from_api_error(
|
|
|
13881
13917
|
"category": error.category,
|
|
13882
13918
|
},
|
|
13883
13919
|
)
|
|
13884
|
-
|
|
13920
|
+
result = _failed(
|
|
13885
13921
|
effective_error_code,
|
|
13886
13922
|
public_message,
|
|
13887
13923
|
recoverable=recoverable,
|
|
@@ -13894,6 +13930,14 @@ def _failed_from_api_error(
|
|
|
13894
13930
|
backend_code=error.backend_code,
|
|
13895
13931
|
http_status=public_http_status,
|
|
13896
13932
|
)
|
|
13933
|
+
if _is_environment_quota_code(error.backend_code):
|
|
13934
|
+
_mark_environment_quota_block(
|
|
13935
|
+
result,
|
|
13936
|
+
write_executed=False,
|
|
13937
|
+
next_action="retry_after_quota_restored",
|
|
13938
|
+
message="backend quota/AI assistant limit blocked this operation; retry after quota is restored",
|
|
13939
|
+
)
|
|
13940
|
+
return result
|
|
13897
13941
|
|
|
13898
13942
|
|
|
13899
13943
|
def _post_write_readback_pending_result(
|
|
@@ -13947,6 +13991,15 @@ def _post_write_readback_pending_result(
|
|
|
13947
13991
|
"next_action": "readback_before_retry",
|
|
13948
13992
|
}
|
|
13949
13993
|
result["verification"]["readback_before_retry"] = True
|
|
13994
|
+
if _is_environment_quota_code(effective_backend_code):
|
|
13995
|
+
result["readback_blocked_by_environment"] = True
|
|
13996
|
+
result["verification"]["readback_blocked_by_environment"] = True
|
|
13997
|
+
_mark_environment_quota_block(
|
|
13998
|
+
result,
|
|
13999
|
+
write_executed=True,
|
|
14000
|
+
next_action="retry_after_quota_restored",
|
|
14001
|
+
message="post-write readback was blocked by backend quota/AI assistant limit; retry readback after quota is restored",
|
|
14002
|
+
)
|
|
13950
14003
|
return result
|
|
13951
14004
|
|
|
13952
14005
|
|
|
@@ -13974,7 +14027,7 @@ def _post_write_may_have_succeeded_result(
|
|
|
13974
14027
|
):
|
|
13975
14028
|
if value is not None:
|
|
13976
14029
|
warning[key] = value
|
|
13977
|
-
|
|
14030
|
+
result = {
|
|
13978
14031
|
"status": "partial_success",
|
|
13979
14032
|
"error_code": error_code,
|
|
13980
14033
|
"recoverable": True,
|
|
@@ -14001,6 +14054,16 @@ def _post_write_may_have_succeeded_result(
|
|
|
14001
14054
|
"safe_to_retry": False,
|
|
14002
14055
|
"next_action": "readback_before_retry",
|
|
14003
14056
|
}
|
|
14057
|
+
if _is_environment_quota_code(effective_backend_code):
|
|
14058
|
+
result["readback_blocked_by_environment"] = True
|
|
14059
|
+
result["verification"]["readback_blocked_by_environment"] = True
|
|
14060
|
+
_mark_environment_quota_block(
|
|
14061
|
+
result,
|
|
14062
|
+
write_executed=True,
|
|
14063
|
+
next_action="retry_after_quota_restored",
|
|
14064
|
+
message="write result or readback was blocked by backend quota/AI assistant limit; retry after quota is restored",
|
|
14065
|
+
)
|
|
14066
|
+
return result
|
|
14004
14067
|
|
|
14005
14068
|
|
|
14006
14069
|
def _readback_transport_error_from_details(details: JSONObject) -> JSONObject | None:
|
|
@@ -14047,6 +14110,43 @@ def _transport_error_payload(error: QingflowApiError) -> JSONObject:
|
|
|
14047
14110
|
}
|
|
14048
14111
|
|
|
14049
14112
|
|
|
14113
|
+
def _is_environment_quota_code(code: Any) -> bool:
|
|
14114
|
+
return backend_code_value_int(code) == 59004
|
|
14115
|
+
|
|
14116
|
+
|
|
14117
|
+
def _mark_environment_quota_block(
|
|
14118
|
+
payload: JSONObject,
|
|
14119
|
+
*,
|
|
14120
|
+
write_executed: bool,
|
|
14121
|
+
next_action: str,
|
|
14122
|
+
message: str,
|
|
14123
|
+
) -> None:
|
|
14124
|
+
payload["environment_blocked"] = True
|
|
14125
|
+
payload["blocker_type"] = "quota_limit"
|
|
14126
|
+
payload["next_action"] = next_action
|
|
14127
|
+
payload["safe_to_retry"] = False
|
|
14128
|
+
payload["write_executed"] = bool(write_executed)
|
|
14129
|
+
details = payload.get("details")
|
|
14130
|
+
if not isinstance(details, dict):
|
|
14131
|
+
details = {}
|
|
14132
|
+
payload["details"] = details
|
|
14133
|
+
details.setdefault("environment_blocked", True)
|
|
14134
|
+
details.setdefault("blocker_type", "quota_limit")
|
|
14135
|
+
details.setdefault("next_action", next_action)
|
|
14136
|
+
details.setdefault("fix_hint", message)
|
|
14137
|
+
warnings = payload.get("warnings")
|
|
14138
|
+
if not isinstance(warnings, list):
|
|
14139
|
+
warnings = []
|
|
14140
|
+
payload["warnings"] = warnings
|
|
14141
|
+
if not any(isinstance(item, dict) and item.get("code") == "ENVIRONMENT_QUOTA_LIMIT" for item in warnings):
|
|
14142
|
+
warning = _warning("ENVIRONMENT_QUOTA_LIMIT", message)
|
|
14143
|
+
for key in ("backend_code", "http_status", "request_id"):
|
|
14144
|
+
value = payload.get(key)
|
|
14145
|
+
if value is not None:
|
|
14146
|
+
warning[key] = value
|
|
14147
|
+
warnings.append(warning)
|
|
14148
|
+
|
|
14149
|
+
|
|
14050
14150
|
def _is_uncertain_write_transport_error(error: QingflowApiError) -> bool:
|
|
14051
14151
|
if is_auth_like_error(error):
|
|
14052
14152
|
return False
|
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
|
|
6
6
|
from ..context import CliContext
|
|
7
7
|
from ..json_io import load_json_value
|
|
8
|
-
from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
|
|
8
|
+
from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -196,7 +196,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
196
196
|
schema_apply_apply.add_argument("--color")
|
|
197
197
|
schema_apply_apply.add_argument("--visibility-file")
|
|
198
198
|
schema_apply_apply.add_argument("--create-if-missing", action="store_true")
|
|
199
|
-
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=
|
|
199
|
+
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
|
|
200
200
|
schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
|
|
201
201
|
schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
202
202
|
schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
@@ -483,8 +483,20 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
483
483
|
apps_warnings = apps_payload.get("warnings") or []
|
|
484
484
|
package_id = args.package_id
|
|
485
485
|
if package_id is None and apps_payload.get("package_id") is not None:
|
|
486
|
-
package_id =
|
|
486
|
+
package_id = _coerce_apps_file_package_id(apps_payload["package_id"])
|
|
487
487
|
if args.apps_file:
|
|
488
|
+
file_create_if_missing = _coerce_apps_file_bool(
|
|
489
|
+
apps_payload.get("create_if_missing"),
|
|
490
|
+
field_name="create_if_missing",
|
|
491
|
+
default=False,
|
|
492
|
+
)
|
|
493
|
+
file_publish = _coerce_apps_file_bool(
|
|
494
|
+
apps_payload.get("publish"),
|
|
495
|
+
field_name="publish",
|
|
496
|
+
default=True,
|
|
497
|
+
)
|
|
498
|
+
effective_create_if_missing = bool(args.create_if_missing or file_create_if_missing)
|
|
499
|
+
effective_publish = bool(args.publish if args.publish is not None else file_publish)
|
|
488
500
|
if not apps:
|
|
489
501
|
raise_config_error(
|
|
490
502
|
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
@@ -505,8 +517,8 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
505
517
|
profile=args.profile,
|
|
506
518
|
package_id=package_id,
|
|
507
519
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
508
|
-
create_if_missing=
|
|
509
|
-
publish=
|
|
520
|
+
create_if_missing=effective_create_if_missing,
|
|
521
|
+
publish=effective_publish,
|
|
510
522
|
apps=apps,
|
|
511
523
|
add_fields=[],
|
|
512
524
|
update_fields=[],
|
|
@@ -543,7 +555,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
543
555
|
color=args.color,
|
|
544
556
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
545
557
|
create_if_missing=bool(args.create_if_missing),
|
|
546
|
-
publish=bool(args.publish),
|
|
558
|
+
publish=True if args.publish is None else bool(args.publish),
|
|
547
559
|
add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
|
|
548
560
|
update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
|
|
549
561
|
remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
|
|
@@ -595,6 +607,10 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
|
|
|
595
607
|
}
|
|
596
608
|
if wrapper.get("package_id") is not None:
|
|
597
609
|
result["package_id"] = wrapper.get("package_id")
|
|
610
|
+
if wrapper.get("create_if_missing") is not None:
|
|
611
|
+
result["create_if_missing"] = wrapper.get("create_if_missing")
|
|
612
|
+
if wrapper.get("publish") is not None:
|
|
613
|
+
result["publish"] = wrapper.get("publish")
|
|
598
614
|
return result
|
|
599
615
|
if any(isinstance(item, dict) and "apps" in item for item in payload):
|
|
600
616
|
raise_config_error(
|
|
@@ -616,6 +632,10 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
|
|
|
616
632
|
result: dict[str, object] = {"apps": apps}
|
|
617
633
|
if payload.get("package_id") is not None:
|
|
618
634
|
result["package_id"] = payload.get("package_id")
|
|
635
|
+
if payload.get("create_if_missing") is not None:
|
|
636
|
+
result["create_if_missing"] = payload.get("create_if_missing")
|
|
637
|
+
if payload.get("publish") is not None:
|
|
638
|
+
result["publish"] = payload.get("publish")
|
|
619
639
|
return result
|
|
620
640
|
raise_config_error(
|
|
621
641
|
"--apps-file must be a JSON array or an object containing apps.",
|
|
@@ -625,6 +645,60 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
|
|
|
625
645
|
)
|
|
626
646
|
|
|
627
647
|
|
|
648
|
+
def _coerce_apps_file_package_id(value: object) -> int:
|
|
649
|
+
package_id: int
|
|
650
|
+
if isinstance(value, bool):
|
|
651
|
+
_raise_apps_file_package_id_invalid(value)
|
|
652
|
+
if isinstance(value, int):
|
|
653
|
+
package_id = value
|
|
654
|
+
elif isinstance(value, str):
|
|
655
|
+
stripped = value.strip()
|
|
656
|
+
try:
|
|
657
|
+
package_id = int(stripped)
|
|
658
|
+
except ValueError:
|
|
659
|
+
_raise_apps_file_package_id_invalid(value)
|
|
660
|
+
else:
|
|
661
|
+
_raise_apps_file_package_id_invalid(value)
|
|
662
|
+
if package_id <= 0:
|
|
663
|
+
_raise_apps_file_package_id_invalid(value)
|
|
664
|
+
return package_id
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _raise_apps_file_package_id_invalid(value: object) -> None:
|
|
668
|
+
raise_config_error(
|
|
669
|
+
"--apps-file package_id must be a positive integer.",
|
|
670
|
+
fix_hint='Use a numeric package_id, for example {"package_id":1001,"apps":[...]}.',
|
|
671
|
+
error_code="APPS_FILE_PACKAGE_ID_INVALID",
|
|
672
|
+
details={
|
|
673
|
+
"field": "package_id",
|
|
674
|
+
"value": value,
|
|
675
|
+
"expected": "positive integer or numeric string",
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _coerce_apps_file_bool(value: object, *, field_name: str, default: bool) -> bool:
|
|
681
|
+
if value is None:
|
|
682
|
+
return default
|
|
683
|
+
if isinstance(value, bool):
|
|
684
|
+
return value
|
|
685
|
+
if isinstance(value, str):
|
|
686
|
+
try:
|
|
687
|
+
return parse_bool_text(value)
|
|
688
|
+
except argparse.ArgumentTypeError:
|
|
689
|
+
pass
|
|
690
|
+
raise_config_error(
|
|
691
|
+
f"--apps-file {field_name} must be a boolean.",
|
|
692
|
+
fix_hint=f'Use "{field_name}": true or "{field_name}": false in --apps-file.',
|
|
693
|
+
error_code="APPS_FILE_BOOLEAN_INVALID",
|
|
694
|
+
details={
|
|
695
|
+
"field": field_name,
|
|
696
|
+
"value": value,
|
|
697
|
+
"expected": "boolean true/false or string true/false/1/0/yes/no",
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
628
702
|
def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
629
703
|
return context.builder.app_layout_apply(
|
|
630
704
|
profile=args.profile,
|
|
@@ -100,7 +100,7 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
100
100
|
details = trimmed.get("details")
|
|
101
101
|
if isinstance(details, dict):
|
|
102
102
|
preserved = {}
|
|
103
|
-
for key in ("blocking_issues", "compiled_match_rules"):
|
|
103
|
+
for key in ("blocking_issues", "compiled_match_rules", "issues", "expected_shape"):
|
|
104
104
|
if key in details:
|
|
105
105
|
preserved[key] = details.get(key)
|
|
106
106
|
compact_details = _compact_scalar_dict(details)
|
|
@@ -184,7 +184,7 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
184
184
|
details = trimmed.get("details")
|
|
185
185
|
if isinstance(details, dict):
|
|
186
186
|
preserved = {}
|
|
187
|
-
for key in ("blocking_issues", "compiled_match_rules"):
|
|
187
|
+
for key in ("blocking_issues", "compiled_match_rules", "issues", "expected_shape"):
|
|
188
188
|
if key in details:
|
|
189
189
|
preserved[key] = details.get(key)
|
|
190
190
|
compact_details = _compact_scalar_dict(details)
|
|
@@ -1071,8 +1071,9 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
|
|
|
1071
1071
|
if isinstance(details, dict):
|
|
1072
1072
|
_drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
1073
1073
|
preserved = {}
|
|
1074
|
-
|
|
1075
|
-
|
|
1074
|
+
for key in ("compiled_match_rules", "issues", "expected_shape"):
|
|
1075
|
+
if key in details:
|
|
1076
|
+
preserved[key] = details.get(key)
|
|
1076
1077
|
compact = _compact_scalar_dict(details)
|
|
1077
1078
|
compact.update(preserved)
|
|
1078
1079
|
if compact:
|