@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.59
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 +3 -2
- package/docs/local-agent-install.md +9 -0
- package/npm/bin/qingflow.mjs +1 -1
- package/npm/lib/runtime.mjs +156 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +137 -5
- package/src/qingflow_mcp/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +19 -16
- package/src/qingflow_mcp/cli/commands/builder.py +124 -162
- package/src/qingflow_mcp/cli/commands/common.py +21 -95
- package/src/qingflow_mcp/cli/commands/imports.py +42 -34
- package/src/qingflow_mcp/cli/commands/record.py +131 -133
- package/src/qingflow_mcp/cli/commands/task.py +43 -44
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +35 -32
- package/src/qingflow_mcp/cli/formatters.py +124 -121
- package/src/qingflow_mcp/cli/main.py +52 -17
- package/src/qingflow_mcp/server_app_builder.py +122 -190
- package/src/qingflow_mcp/server_app_user.py +63 -662
- package/src/qingflow_mcp/tools/solution_tools.py +95 -3
- package/src/qingflow_mcp/ops/__init__.py +0 -3
- package/src/qingflow_mcp/ops/apps.py +0 -64
- package/src/qingflow_mcp/ops/auth.py +0 -121
- package/src/qingflow_mcp/ops/base.py +0 -290
- package/src/qingflow_mcp/ops/builder.py +0 -357
- package/src/qingflow_mcp/ops/context.py +0 -120
- package/src/qingflow_mcp/ops/directory.py +0 -171
- package/src/qingflow_mcp/ops/feedback.py +0 -49
- package/src/qingflow_mcp/ops/files.py +0 -78
- package/src/qingflow_mcp/ops/imports.py +0 -140
- package/src/qingflow_mcp/ops/records.py +0 -415
- package/src/qingflow_mcp/ops/tasks.py +0 -171
- package/src/qingflow_mcp/ops/workspace.py +0 -76
|
@@ -3,11 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any, TextIO
|
|
5
5
|
|
|
6
|
-
from ..ops.base import public_result
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
10
|
-
result = public_result(result)
|
|
11
8
|
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
12
9
|
text = formatter(result)
|
|
13
10
|
stream.write(text)
|
|
@@ -15,65 +12,61 @@ def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> No
|
|
|
15
12
|
stream.write("\n")
|
|
16
13
|
|
|
17
14
|
|
|
18
|
-
def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
|
|
19
|
-
json.dump(public_result(result), stream, ensure_ascii=False, indent=2)
|
|
20
|
-
stream.write("\n")
|
|
21
|
-
|
|
22
|
-
|
|
23
15
|
def _format_generic(result: dict[str, Any]) -> str:
|
|
24
|
-
lines = [
|
|
16
|
+
lines: list[str] = []
|
|
17
|
+
title = _first_present(result, "status", "message")
|
|
18
|
+
if title:
|
|
19
|
+
lines.append(str(title))
|
|
25
20
|
data = result.get("data")
|
|
26
21
|
if isinstance(data, dict):
|
|
27
|
-
|
|
22
|
+
scalar_lines = _dict_scalar_lines(data)
|
|
23
|
+
if scalar_lines:
|
|
24
|
+
lines.extend(scalar_lines)
|
|
25
|
+
elif result:
|
|
26
|
+
scalar_lines = _dict_scalar_lines(result)
|
|
27
|
+
if scalar_lines:
|
|
28
|
+
lines.extend(scalar_lines)
|
|
29
|
+
if not lines:
|
|
30
|
+
lines.append(json.dumps(result, ensure_ascii=False, indent=2))
|
|
28
31
|
_append_warnings(lines, result.get("warnings"))
|
|
32
|
+
_append_verification(lines, result.get("verification"))
|
|
29
33
|
return "\n".join(lines) + "\n"
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
def
|
|
33
|
-
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
34
|
-
user = data.get("user") if isinstance(data.get("user"), dict) else {}
|
|
35
|
-
workspace = data.get("workspace") if isinstance(data.get("workspace"), dict) else {}
|
|
36
|
+
def _format_whoami(result: dict[str, Any]) -> str:
|
|
36
37
|
lines = [
|
|
37
|
-
f"Profile: {
|
|
38
|
-
f"User: {
|
|
39
|
-
f"UID: {
|
|
40
|
-
f"Workspace: {
|
|
41
|
-
f"Base URL: {
|
|
38
|
+
f"Profile: {result.get('profile')}",
|
|
39
|
+
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
40
|
+
f"UID: {result.get('uid')}",
|
|
41
|
+
f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
|
|
42
|
+
f"Base URL: {result.get('base_url')}",
|
|
42
43
|
]
|
|
43
44
|
return "\n".join(lines) + "\n"
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
47
|
-
|
|
48
|
-
items =
|
|
49
|
-
rows = [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
page = result.get("page") if isinstance(result.get("page"), dict) else {}
|
|
49
|
+
items = page.get("list") if isinstance(page.get("list"), list) else []
|
|
50
|
+
rows = []
|
|
51
|
+
for item in items:
|
|
52
|
+
if not isinstance(item, dict):
|
|
53
|
+
continue
|
|
54
|
+
rows.append(
|
|
55
|
+
[
|
|
56
|
+
str(item.get("wsId") or ""),
|
|
57
|
+
str(item.get("workspaceName") or item.get("wsName") or ""),
|
|
58
|
+
str(item.get("remark") or ""),
|
|
59
|
+
]
|
|
60
|
+
)
|
|
58
61
|
return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
59
62
|
|
|
60
63
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
f"WS ID: {workspace.get('ws_id') or '-'}",
|
|
66
|
-
f"QF Version: {result.get('data', {}).get('qf_version') or '-'}",
|
|
67
|
-
f"QF Version Source: {result.get('data', {}).get('qf_version_source') or '-'}",
|
|
68
|
-
]
|
|
69
|
-
return "\n".join(lines) + "\n"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _format_apps_list(result: dict[str, Any]) -> str:
|
|
73
|
-
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
74
|
-
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
64
|
+
def _format_app_items(result: dict[str, Any]) -> str:
|
|
65
|
+
items = result.get("items")
|
|
66
|
+
if not isinstance(items, list):
|
|
67
|
+
items = result.get("apps")
|
|
75
68
|
rows = []
|
|
76
|
-
for item in items:
|
|
69
|
+
for item in items or []:
|
|
77
70
|
if not isinstance(item, dict):
|
|
78
71
|
continue
|
|
79
72
|
rows.append(
|
|
@@ -86,7 +79,7 @@ def _format_apps_list(result: dict[str, Any]) -> str:
|
|
|
86
79
|
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
87
80
|
|
|
88
81
|
|
|
89
|
-
def
|
|
82
|
+
def _format_app_get(result: dict[str, Any]) -> str:
|
|
90
83
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
91
84
|
lines = [
|
|
92
85
|
f"App: {data.get('app_name') or '-'}",
|
|
@@ -97,7 +90,8 @@ def _format_app_show(result: dict[str, Any]) -> str:
|
|
|
97
90
|
if isinstance(import_capability, dict):
|
|
98
91
|
lines.append(
|
|
99
92
|
"Import Capability: "
|
|
100
|
-
f"{import_capability.get('auth_source') or 'unknown'} /
|
|
93
|
+
f"{import_capability.get('auth_source') or 'unknown'} / "
|
|
94
|
+
f"can_import={import_capability.get('can_import')}"
|
|
101
95
|
)
|
|
102
96
|
views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
|
|
103
97
|
lines.append(f"Accessible Views: {len(views)}")
|
|
@@ -108,7 +102,7 @@ def _format_app_show(result: dict[str, Any]) -> str:
|
|
|
108
102
|
return "\n".join(lines) + "\n"
|
|
109
103
|
|
|
110
104
|
|
|
111
|
-
def
|
|
105
|
+
def _format_record_list(result: dict[str, Any]) -> str:
|
|
112
106
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
113
107
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
114
108
|
lines = [f"Returned Records: {len(items)}"]
|
|
@@ -118,43 +112,40 @@ def _format_records_list(result: dict[str, Any]) -> str:
|
|
|
118
112
|
if len(items) > 10:
|
|
119
113
|
lines.append(f"... {len(items) - 10} more")
|
|
120
114
|
_append_warnings(lines, result.get("warnings"))
|
|
115
|
+
_append_verification(lines, result.get("verification"))
|
|
121
116
|
return "\n".join(lines) + "\n"
|
|
122
117
|
|
|
123
118
|
|
|
124
|
-
def
|
|
125
|
-
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
126
|
-
record = data.get("record")
|
|
127
|
-
if isinstance(record, dict):
|
|
128
|
-
lines = [f"Record ID: {data.get('record_id') or '-'}"]
|
|
129
|
-
lines.extend(_scalar_lines(record, limit=12))
|
|
130
|
-
_append_warnings(lines, result.get("warnings"))
|
|
131
|
-
return "\n".join(lines) + "\n"
|
|
132
|
-
return _format_generic(result)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _format_record_write(result: dict[str, Any]) -> str:
|
|
119
|
+
def _format_task_list(result: dict[str, Any]) -> str:
|
|
136
120
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
121
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
122
|
+
rows = []
|
|
123
|
+
for item in items:
|
|
124
|
+
if not isinstance(item, dict):
|
|
125
|
+
continue
|
|
126
|
+
rows.append(
|
|
127
|
+
[
|
|
128
|
+
str(item.get("app_key") or ""),
|
|
129
|
+
str(item.get("record_id") or ""),
|
|
130
|
+
str(item.get("workflow_node_id") or ""),
|
|
131
|
+
str(item.get("title") or item.get("task_name") or ""),
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
|
|
135
|
+
lines = output.rstrip("\n").split("\n")
|
|
145
136
|
_append_warnings(lines, result.get("warnings"))
|
|
146
137
|
return "\n".join(lines) + "\n"
|
|
147
138
|
|
|
148
139
|
|
|
149
140
|
def _format_import_verify(result: dict[str, Any]) -> str:
|
|
150
|
-
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
151
141
|
lines = [
|
|
152
|
-
f"
|
|
153
|
-
f"
|
|
154
|
-
f"
|
|
155
|
-
f"
|
|
142
|
+
f"App Key: {result.get('app_key') or '-'}",
|
|
143
|
+
f"File: {result.get('file_name') or result.get('file_path') or '-'}",
|
|
144
|
+
f"Can Import: {result.get('can_import')}",
|
|
145
|
+
f"Apply Rows: {result.get('apply_rows')}",
|
|
146
|
+
f"Verification ID: {result.get('verification_id') or '-'}",
|
|
156
147
|
]
|
|
157
|
-
issues =
|
|
148
|
+
issues = result.get("issues") if isinstance(result.get("issues"), list) else []
|
|
158
149
|
if issues:
|
|
159
150
|
lines.append("Issues:")
|
|
160
151
|
for issue in issues:
|
|
@@ -163,53 +154,50 @@ def _format_import_verify(result: dict[str, Any]) -> str:
|
|
|
163
154
|
else:
|
|
164
155
|
lines.append(f"- {issue}")
|
|
165
156
|
_append_warnings(lines, result.get("warnings"))
|
|
157
|
+
_append_verification(lines, result.get("verification"))
|
|
166
158
|
return "\n".join(lines) + "\n"
|
|
167
159
|
|
|
168
160
|
|
|
169
161
|
def _format_import_status(result: dict[str, Any]) -> str:
|
|
170
|
-
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
171
162
|
lines = [
|
|
172
|
-
f"Status: {
|
|
173
|
-
f"Import ID: {
|
|
174
|
-
f"Process ID: {
|
|
175
|
-
f"Success Rows: {
|
|
176
|
-
f"Failed Rows: {
|
|
177
|
-
f"Progress: {
|
|
163
|
+
f"Status: {result.get('status') or '-'}",
|
|
164
|
+
f"Import ID: {result.get('import_id') or '-'}",
|
|
165
|
+
f"Process ID: {result.get('process_id_str') or '-'}",
|
|
166
|
+
f"Success Rows: {result.get('success_rows') or 0}",
|
|
167
|
+
f"Failed Rows: {result.get('failed_rows') or 0}",
|
|
168
|
+
f"Progress: {result.get('progress') or '-'}",
|
|
178
169
|
]
|
|
179
170
|
_append_warnings(lines, result.get("warnings"))
|
|
171
|
+
_append_verification(lines, result.get("verification"))
|
|
180
172
|
return "\n".join(lines) + "\n"
|
|
181
173
|
|
|
182
174
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
str(item.get("record_id") or ""),
|
|
194
|
-
str(item.get("workflow_node_id") or ""),
|
|
195
|
-
str(item.get("title") or ""),
|
|
196
|
-
]
|
|
197
|
-
)
|
|
198
|
-
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
|
|
199
|
-
lines = output.rstrip("\n").split("\n")
|
|
200
|
-
_append_warnings(lines, result.get("warnings"))
|
|
201
|
-
return "\n".join(lines) + "\n"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _format_builder_result(result: dict[str, Any]) -> str:
|
|
205
|
-
lines = [f"Code: {result.get('code')}", f"Message: {result.get('message')}"]
|
|
175
|
+
def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
176
|
+
lines = []
|
|
177
|
+
if "status" in result:
|
|
178
|
+
lines.append(f"Status: {result.get('status')}")
|
|
179
|
+
if "app_key" in result:
|
|
180
|
+
lines.append(f"App Key: {result.get('app_key')}")
|
|
181
|
+
if "dash_key" in result:
|
|
182
|
+
lines.append(f"Dash Key: {result.get('dash_key')}")
|
|
183
|
+
if "verified" in result:
|
|
184
|
+
lines.append(f"Verified: {result.get('verified')}")
|
|
206
185
|
data = result.get("data")
|
|
207
186
|
if isinstance(data, dict):
|
|
208
|
-
|
|
187
|
+
scalar_lines = _dict_scalar_lines(data)
|
|
188
|
+
lines.extend(scalar_lines[:8])
|
|
209
189
|
_append_warnings(lines, result.get("warnings"))
|
|
190
|
+
_append_verification(lines, result.get("verification"))
|
|
191
|
+
if not lines:
|
|
192
|
+
return _format_generic(result)
|
|
210
193
|
return "\n".join(lines) + "\n"
|
|
211
194
|
|
|
212
195
|
|
|
196
|
+
def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
|
|
197
|
+
json.dump(result, stream, ensure_ascii=False, indent=2)
|
|
198
|
+
stream.write("\n")
|
|
199
|
+
|
|
200
|
+
|
|
213
201
|
def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
|
|
214
202
|
lines = [title]
|
|
215
203
|
if not rows:
|
|
@@ -219,20 +207,19 @@ def _render_titled_table(title: str, headers: list[str], rows: list[list[str]])
|
|
|
219
207
|
for row in rows:
|
|
220
208
|
for index, cell in enumerate(row):
|
|
221
209
|
widths[index] = max(widths[index], len(cell))
|
|
222
|
-
|
|
210
|
+
header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
|
|
211
|
+
lines.append(header_line)
|
|
223
212
|
lines.append(" ".join("-" * width for width in widths))
|
|
224
213
|
for row in rows:
|
|
225
214
|
lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
|
|
226
215
|
return "\n".join(lines) + "\n"
|
|
227
216
|
|
|
228
217
|
|
|
229
|
-
def
|
|
218
|
+
def _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
|
|
230
219
|
lines: list[str] = []
|
|
231
220
|
for key, value in payload.items():
|
|
232
221
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
233
222
|
lines.append(f"{key}: {value}")
|
|
234
|
-
if len(lines) >= limit:
|
|
235
|
-
break
|
|
236
223
|
return lines
|
|
237
224
|
|
|
238
225
|
|
|
@@ -244,23 +231,39 @@ def _append_warnings(lines: list[str], warnings: Any) -> None:
|
|
|
244
231
|
if isinstance(warning, dict):
|
|
245
232
|
code = warning.get("code")
|
|
246
233
|
message = warning.get("message")
|
|
247
|
-
|
|
234
|
+
if code or message:
|
|
235
|
+
lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
|
|
236
|
+
else:
|
|
237
|
+
lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
|
|
248
238
|
else:
|
|
249
239
|
lines.append(f"- {warning}")
|
|
250
240
|
|
|
251
241
|
|
|
242
|
+
def _append_verification(lines: list[str], verification: Any) -> None:
|
|
243
|
+
if not isinstance(verification, dict) or not verification:
|
|
244
|
+
return
|
|
245
|
+
lines.append("Verification:")
|
|
246
|
+
for key, value in verification.items():
|
|
247
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
248
|
+
lines.append(f"- {key}: {value}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _first_present(payload: dict[str, Any], *keys: str) -> Any:
|
|
252
|
+
for key in keys:
|
|
253
|
+
if key in payload and payload.get(key) is not None:
|
|
254
|
+
return payload.get(key)
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
252
258
|
_FORMATTERS = {
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"record_show": _format_record_show,
|
|
261
|
-
"record_write": _format_record_write,
|
|
259
|
+
"auth_whoami": _format_whoami,
|
|
260
|
+
"workspace_list": _format_workspace_list,
|
|
261
|
+
"app_list": _format_app_items,
|
|
262
|
+
"app_search": _format_app_items,
|
|
263
|
+
"app_get": _format_app_get,
|
|
264
|
+
"record_list": _format_record_list,
|
|
265
|
+
"task_list": _format_task_list,
|
|
262
266
|
"import_verify": _format_import_verify,
|
|
263
267
|
"import_status": _format_import_status,
|
|
264
|
-
"
|
|
265
|
-
"builder_result": _format_builder_result,
|
|
268
|
+
"builder_summary": _format_builder_summary,
|
|
266
269
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
import sys
|
|
5
6
|
from typing import Any, Callable, TextIO
|
|
6
7
|
|
|
7
8
|
from ..errors import QingflowApiError
|
|
8
|
-
from ..ops.base import normalize_exception
|
|
9
|
-
from .commands import register_all_commands
|
|
10
9
|
from .context import CliContext, build_cli_context
|
|
11
10
|
from .formatters import emit_json_result, emit_text_result
|
|
11
|
+
from .commands import register_all_commands
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
-
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI
|
|
18
|
+
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
19
19
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
20
|
-
parser.add_argument("--json", action="store_true", help="
|
|
20
|
+
parser.add_argument("--json", action="store_true", help="输出原始 JSON")
|
|
21
21
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
22
|
register_all_commands(subparsers)
|
|
23
23
|
return parser
|
|
@@ -46,16 +46,15 @@ def run(
|
|
|
46
46
|
if handler is None:
|
|
47
47
|
parser.print_help(out)
|
|
48
48
|
return 2
|
|
49
|
-
|
|
50
49
|
context = context_factory()
|
|
51
50
|
try:
|
|
52
51
|
result = handler(args, context)
|
|
53
52
|
except RuntimeError as exc:
|
|
54
|
-
|
|
53
|
+
payload = _parse_error_payload(exc)
|
|
54
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
55
55
|
except QingflowApiError as exc:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
result = normalize_exception(exc)
|
|
56
|
+
payload = exc.to_dict()
|
|
57
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
59
58
|
finally:
|
|
60
59
|
context.close()
|
|
61
60
|
|
|
@@ -95,17 +94,53 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
95
94
|
return global_args + remaining
|
|
96
95
|
|
|
97
96
|
|
|
97
|
+
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
98
|
+
raw = str(exc)
|
|
99
|
+
try:
|
|
100
|
+
payload = json.loads(raw)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
return {"category": "runtime", "message": raw}
|
|
103
|
+
return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
|
|
107
|
+
exit_code = _error_exit_code(payload)
|
|
108
|
+
if json_mode:
|
|
109
|
+
emit_json_result(payload, stream=stdout)
|
|
110
|
+
return exit_code
|
|
111
|
+
lines = [
|
|
112
|
+
f"Category: {payload.get('category') or 'error'}",
|
|
113
|
+
f"Message: {payload.get('message') or 'Unknown error'}",
|
|
114
|
+
]
|
|
115
|
+
if payload.get("backend_code") is not None:
|
|
116
|
+
lines.append(f"Backend Code: {payload.get('backend_code')}")
|
|
117
|
+
if payload.get("request_id"):
|
|
118
|
+
lines.append(f"Request ID: {payload.get('request_id')}")
|
|
119
|
+
details = payload.get("details")
|
|
120
|
+
if isinstance(details, dict):
|
|
121
|
+
for key, value in details.items():
|
|
122
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
123
|
+
lines.append(f"{key}: {value}")
|
|
124
|
+
stderr.write("\n".join(lines) + "\n")
|
|
125
|
+
return exit_code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _error_exit_code(payload: dict[str, Any]) -> int:
|
|
129
|
+
category = str(payload.get("category") or "").lower()
|
|
130
|
+
if category in {"auth", "workspace"}:
|
|
131
|
+
return 3
|
|
132
|
+
return 4
|
|
133
|
+
|
|
134
|
+
|
|
98
135
|
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
99
136
|
if not isinstance(result, dict):
|
|
100
137
|
return 0
|
|
101
|
-
if result.get("ok") is
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
return 2
|
|
108
|
-
return 4
|
|
138
|
+
if result.get("ok") is False:
|
|
139
|
+
return 4
|
|
140
|
+
status = str(result.get("status") or "").lower()
|
|
141
|
+
if status in {"failed", "blocked"}:
|
|
142
|
+
return 4
|
|
143
|
+
return 0
|
|
109
144
|
|
|
110
145
|
|
|
111
146
|
if __name__ == "__main__":
|