buildify-cli 1.0.0__py3-none-any.whl
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.
- buildify_cli/__init__.py +1 -0
- buildify_cli/__main__.py +4 -0
- buildify_cli/cli.py +1206 -0
- buildify_cli/client.py +288 -0
- buildify_cli/config.py +593 -0
- buildify_cli/credential_form.py +156 -0
- buildify_cli/exit_codes.py +8 -0
- buildify_cli/output.py +46 -0
- buildify_cli/publish_view.py +122 -0
- buildify_cli/usage.md +245 -0
- buildify_cli-1.0.0.dist-info/METADATA +95 -0
- buildify_cli-1.0.0.dist-info/RECORD +15 -0
- buildify_cli-1.0.0.dist-info/WHEEL +5 -0
- buildify_cli-1.0.0.dist-info/entry_points.txt +2 -0
- buildify_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
UI_ONLY = frozenset({"Space", "TestButton"})
|
|
7
|
+
SECRET_UI = frozenset({"Password"})
|
|
8
|
+
|
|
9
|
+
CREDENTIAL_INPUT_REQUIRED = "credential_input_required"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def default_credential_name(bundle_name: str, type_name: str) -> str:
|
|
13
|
+
"""Machine-safe instance name. Agent must display this and wait for confirmation."""
|
|
14
|
+
slug = (bundle_name or "").split("/")[-1].strip().lower()
|
|
15
|
+
slug = re.sub(r"[^a-z0-9]+", "-", slug).strip("-")
|
|
16
|
+
if slug:
|
|
17
|
+
return slug
|
|
18
|
+
fallback = re.sub(r"[^a-z0-9]+", "-", (type_name or "").lower()).strip("-")
|
|
19
|
+
return fallback or "credential"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_credential_label(type_label: Optional[str], type_name: str) -> str:
|
|
23
|
+
label = (type_label or "").strip()
|
|
24
|
+
return label or (type_name or "").strip() or "访问凭证"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_secret_field(field: dict[str, Any]) -> bool:
|
|
28
|
+
ui = str(field.get("uiComponent") or "")
|
|
29
|
+
if ui in SECRET_UI:
|
|
30
|
+
return True
|
|
31
|
+
options = field.get("typeOptions")
|
|
32
|
+
if isinstance(options, dict):
|
|
33
|
+
if options.get("password") is True:
|
|
34
|
+
return True
|
|
35
|
+
if str(options.get("type") or "").lower() == "password":
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def form_fields(schema: Any) -> list[dict[str, Any]]:
|
|
41
|
+
"""Leaf credential form fields (skip TestButton / _prefixed)."""
|
|
42
|
+
if not isinstance(schema, dict):
|
|
43
|
+
return []
|
|
44
|
+
properties = schema.get("properties")
|
|
45
|
+
if not isinstance(properties, list):
|
|
46
|
+
return []
|
|
47
|
+
out: list[dict[str, Any]] = []
|
|
48
|
+
for raw in properties:
|
|
49
|
+
if not isinstance(raw, dict):
|
|
50
|
+
continue
|
|
51
|
+
name = str(raw.get("name") or "").strip()
|
|
52
|
+
if not name or name.startswith("_"):
|
|
53
|
+
continue
|
|
54
|
+
ui = str(raw.get("uiComponent") or "")
|
|
55
|
+
if ui in UI_ONLY:
|
|
56
|
+
continue
|
|
57
|
+
out.append(
|
|
58
|
+
{
|
|
59
|
+
"name": name,
|
|
60
|
+
"label": str(raw.get("label") or name),
|
|
61
|
+
"required": bool(raw.get("required")),
|
|
62
|
+
"secret": is_secret_field(raw),
|
|
63
|
+
"uiComponent": ui or None,
|
|
64
|
+
"placeholder": raw.get("placeholder"),
|
|
65
|
+
"default": raw.get("default"),
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def user_message(
|
|
72
|
+
*,
|
|
73
|
+
project_name: str,
|
|
74
|
+
bundle_name: str,
|
|
75
|
+
type_name: str,
|
|
76
|
+
type_label: str,
|
|
77
|
+
default_name: str,
|
|
78
|
+
default_label: str,
|
|
79
|
+
fields: list[dict[str, Any]],
|
|
80
|
+
missing: list[str],
|
|
81
|
+
) -> str:
|
|
82
|
+
lines = [
|
|
83
|
+
"创建访问凭证前需要你确认项目,并提供表单参数。",
|
|
84
|
+
"",
|
|
85
|
+
f"项目:{project_name}",
|
|
86
|
+
f"软件包:{bundle_name}",
|
|
87
|
+
f"类型:{type_label or type_name}({type_name})",
|
|
88
|
+
"",
|
|
89
|
+
"建议的名称(可改,确认前不会创建):",
|
|
90
|
+
f" - name(引用名):{default_name}",
|
|
91
|
+
f" - label(显示名):{default_label}",
|
|
92
|
+
"",
|
|
93
|
+
"请提供以下参数(密钥类字段不会被回显):",
|
|
94
|
+
]
|
|
95
|
+
if not fields:
|
|
96
|
+
lines.append(" (该类型没有表单字段)")
|
|
97
|
+
for field in fields:
|
|
98
|
+
flag = "必填" if field.get("required") else "可选"
|
|
99
|
+
secret = ",密钥" if field.get("secret") else ""
|
|
100
|
+
lines.append(f" - {field['label']}({field['name']},{flag}{secret})")
|
|
101
|
+
if missing:
|
|
102
|
+
lines.append("")
|
|
103
|
+
lines.append("还缺:" + "、".join(missing))
|
|
104
|
+
lines.extend(
|
|
105
|
+
[
|
|
106
|
+
"",
|
|
107
|
+
"请确认或修改 name / label,并给出参数值。确认后再创建。",
|
|
108
|
+
"不要把密钥写进仓库或命令行参数;用 stdin:",
|
|
109
|
+
" printf '%s' '{\"host\":\"...\"}' | buildify --json project create-credential \\",
|
|
110
|
+
" -p <项目> -b <bundle> --type <类型> --name <name> --label <label> --file -",
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def prompt_payload(
|
|
117
|
+
*,
|
|
118
|
+
project_id: str,
|
|
119
|
+
project_name: str,
|
|
120
|
+
bundle_name: str,
|
|
121
|
+
bundle_version: Optional[str],
|
|
122
|
+
type_name: str,
|
|
123
|
+
type_label: str,
|
|
124
|
+
default_name: str,
|
|
125
|
+
default_label: str,
|
|
126
|
+
fields: list[dict[str, Any]],
|
|
127
|
+
missing: list[str],
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
return {
|
|
130
|
+
"reason": CREDENTIAL_INPUT_REQUIRED,
|
|
131
|
+
"projectId": project_id,
|
|
132
|
+
"projectName": project_name,
|
|
133
|
+
"bundleName": bundle_name,
|
|
134
|
+
"bundleVersion": bundle_version,
|
|
135
|
+
"type": type_name,
|
|
136
|
+
"typeLabel": type_label,
|
|
137
|
+
"defaults": {"name": default_name, "label": default_label},
|
|
138
|
+
"missing": missing,
|
|
139
|
+
"parameters": fields,
|
|
140
|
+
"userMessage": user_message(
|
|
141
|
+
project_name=project_name,
|
|
142
|
+
bundle_name=bundle_name,
|
|
143
|
+
type_name=type_name,
|
|
144
|
+
type_label=type_label,
|
|
145
|
+
default_name=default_name,
|
|
146
|
+
default_label=default_label,
|
|
147
|
+
fields=fields,
|
|
148
|
+
missing=missing,
|
|
149
|
+
),
|
|
150
|
+
"agentInstructions": (
|
|
151
|
+
"Stop. Show data.userMessage (includes default name/label). Ask the user to "
|
|
152
|
+
"confirm or change name/label and to provide parameter values. Do not create "
|
|
153
|
+
"until they confirm. Then POST via stdin --file - ; never echo secrets, never "
|
|
154
|
+
"pass --data on argv, never write secrets into the repo."
|
|
155
|
+
),
|
|
156
|
+
}
|
buildify_cli/output.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Optional, TextIO
|
|
7
|
+
|
|
8
|
+
SUCCESS_BODY_CODE = 200
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_tty(stream: TextIO) -> bool:
|
|
12
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def os_no_color() -> bool:
|
|
16
|
+
return os.environ.get("NO_COLOR", "").strip() != ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_json(
|
|
20
|
+
*,
|
|
21
|
+
ok: bool,
|
|
22
|
+
code: int,
|
|
23
|
+
msg: str,
|
|
24
|
+
http_status: int,
|
|
25
|
+
data: Any = None,
|
|
26
|
+
stream: Optional[TextIO] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
payload = {
|
|
29
|
+
"ok": ok,
|
|
30
|
+
"code": code,
|
|
31
|
+
"msg": msg,
|
|
32
|
+
"http_status": http_status,
|
|
33
|
+
"data": data,
|
|
34
|
+
}
|
|
35
|
+
out = sys.stdout if stream is None else stream
|
|
36
|
+
out.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
37
|
+
out.flush()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def human_err(message: str) -> None:
|
|
41
|
+
sys.stderr.write(message + "\n")
|
|
42
|
+
sys.stderr.flush()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def body_success(code: Optional[int]) -> bool:
|
|
46
|
+
return code is None or code == SUCCESS_BODY_CODE
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
_STATUS_ICON = {
|
|
6
|
+
"success": "✅",
|
|
7
|
+
"success_with_warnings": "⚠️",
|
|
8
|
+
"failed": "❌",
|
|
9
|
+
"timeout": "⏰",
|
|
10
|
+
"running": "🔄",
|
|
11
|
+
"canceled": "🚫",
|
|
12
|
+
"pending": "⏳",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_STATUS_TITLE = {
|
|
16
|
+
"success": "已发布",
|
|
17
|
+
"success_with_warnings": "已发布(有警告)",
|
|
18
|
+
"failed": "发布失败",
|
|
19
|
+
"timeout": "发布超时",
|
|
20
|
+
"running": "发布中",
|
|
21
|
+
"canceled": "已取消",
|
|
22
|
+
"pending": "发布中",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_STATUS_LABEL = {
|
|
26
|
+
"success": "成功",
|
|
27
|
+
"success_with_warnings": "成功(有警告)",
|
|
28
|
+
"failed": "失败",
|
|
29
|
+
"timeout": "超时",
|
|
30
|
+
"running": "部署中",
|
|
31
|
+
"canceled": "已取消",
|
|
32
|
+
"pending": "待部署",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_publish_payload(payload: Any) -> bool:
|
|
37
|
+
return isinstance(payload, dict) and isinstance(payload.get("summary"), dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_publish_report(payload: dict[str, Any]) -> str:
|
|
41
|
+
"""Markdown tables + status icons for deploy / published / deployment responses."""
|
|
42
|
+
summary = payload.get("summary") or {}
|
|
43
|
+
status = _status_key(payload.get("deployStatus"))
|
|
44
|
+
icon = payload.get("deployStatusIcon") or _STATUS_ICON.get(status, "⏳")
|
|
45
|
+
label = payload.get("deployStatusLabel") or _STATUS_LABEL.get(status, "待部署")
|
|
46
|
+
title = _STATUS_TITLE.get(status, "发布")
|
|
47
|
+
|
|
48
|
+
project = _cell(summary.get("projectName"))
|
|
49
|
+
workflow = _cell(summary.get("workflowName"))
|
|
50
|
+
version = summary.get("versionName") or payload.get("versionName") or "—"
|
|
51
|
+
publish_remark = summary.get("publishRemark") or payload.get("publishRemark") or "—"
|
|
52
|
+
published_at = payload.get("publishedAt") or payload.get("createdAt") or "—"
|
|
53
|
+
|
|
54
|
+
lines: list[str] = [
|
|
55
|
+
f"## {icon} {title}",
|
|
56
|
+
"",
|
|
57
|
+
"| 项 | 内容 |",
|
|
58
|
+
"| --- | --- |",
|
|
59
|
+
f"| 项目 | {project} |",
|
|
60
|
+
f"| 流程 | {workflow} |",
|
|
61
|
+
f"| 版本号 | `{_cell(version)}` |",
|
|
62
|
+
f"| 发布说明 | {_cell(publish_remark)} |",
|
|
63
|
+
f"| 总体 | {icon} {label} |",
|
|
64
|
+
]
|
|
65
|
+
if published_at and published_at != "—":
|
|
66
|
+
lines.append(f"| 发布时间 | {published_at} |")
|
|
67
|
+
|
|
68
|
+
servers = summary.get("servers") or []
|
|
69
|
+
lines.extend(["", "### 服务器", ""])
|
|
70
|
+
if servers:
|
|
71
|
+
lines.append("| 服务器 | 描述 | 在线 | 部署 | 说明 |")
|
|
72
|
+
lines.append("| --- | --- | --- | --- | --- |")
|
|
73
|
+
for server in servers:
|
|
74
|
+
if not isinstance(server, dict):
|
|
75
|
+
continue
|
|
76
|
+
online = bool(server.get("isOnline"))
|
|
77
|
+
online_icon = server.get("onlineIcon") or ("🟢" if online else "⚫")
|
|
78
|
+
online_label = server.get("onlineLabel") or ("在线" if online else "离线")
|
|
79
|
+
st = _status_key(server.get("deployStatus"))
|
|
80
|
+
st_icon = server.get("deployStatusIcon") or _STATUS_ICON.get(st, "⏳")
|
|
81
|
+
st_label = server.get("deployStatusLabel") or _STATUS_LABEL.get(st, "待部署")
|
|
82
|
+
remark = server.get("remark") or "无描述"
|
|
83
|
+
message = server.get("message") or "—"
|
|
84
|
+
lines.append(
|
|
85
|
+
f"| {_cell(server.get('workerName'))} | {_cell(remark)} | "
|
|
86
|
+
f"{online_icon} {online_label} | {st_icon} {st_label} | {_cell(message)} |"
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
lines.append("*未绑定服务器*")
|
|
90
|
+
|
|
91
|
+
creds = summary.get("credentials") or []
|
|
92
|
+
lines.extend(["", "### 凭证", ""])
|
|
93
|
+
if creds:
|
|
94
|
+
lines.append("| Bundle | 类型 | name | 名称 |")
|
|
95
|
+
lines.append("| --- | --- | --- | --- |")
|
|
96
|
+
for cred in creds:
|
|
97
|
+
if not isinstance(cred, dict):
|
|
98
|
+
continue
|
|
99
|
+
lines.append(
|
|
100
|
+
f"| {_cell(cred.get('bundleName'))} | {_cell(cred.get('type'))} | "
|
|
101
|
+
f"{_cell(cred.get('name'))} | {_cell(cred.get('label'))} |"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
lines.append("*本流程未引用访问凭证*")
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _status_key(value: Any) -> str:
|
|
110
|
+
if value is None:
|
|
111
|
+
return "pending"
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
return value.strip().lower() or "pending"
|
|
114
|
+
name = getattr(value, "name", None)
|
|
115
|
+
if isinstance(name, str):
|
|
116
|
+
return name.strip().lower()
|
|
117
|
+
return str(value).strip().lower() or "pending"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cell(value: Optional[Any]) -> str:
|
|
121
|
+
text = "—" if value is None or str(value).strip() == "" else str(value).strip()
|
|
122
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
buildify_cli/usage.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Buildify open API CLI — quick reference
|
|
2
|
+
|
|
3
|
+
Authoring workflows from a terminal: discover installed bundle nodes, read their form schemas
|
|
4
|
+
and docs, reference existing credentials, then create / validate / test run / deploy a flow.
|
|
5
|
+
|
|
6
|
+
**Default API base URL:** `https://openapi.buildify.cn` (override with `--base-url` or `BUILDIFY_OPENAPI_BASE_URL`).
|
|
7
|
+
|
|
8
|
+
**Auth:** `Authorization: Bearer <keyId>.<secret>`. Keys are **issued and revoked in the console**
|
|
9
|
+
(workspace settings → Open API keys), not via this CLI. One key is bound to exactly one workspace;
|
|
10
|
+
every command below is implicitly scoped to it. The plaintext is shown **only once** at creation.
|
|
11
|
+
|
|
12
|
+
If no key is configured:
|
|
13
|
+
|
|
14
|
+
- **TTY** (not `--json`): print how to create the key, hidden-prompt for it, save to `~/.buildifyrc`
|
|
15
|
+
with mode `600`, then continue.
|
|
16
|
+
- **`--json` / CI / pipe:** exit 2. JSON `data.reason` is `missing_api_key`; `data.userMessage` is
|
|
17
|
+
the Chinese how-to; `data.agentInstructions` tells agents to ask the user and save via stdin.
|
|
18
|
+
|
|
19
|
+
Several workspaces mean several keys. Save each with `config add-profile <name>` (this does **not**
|
|
20
|
+
change the default). A repo may commit `.buildify-workspace` containing only the public workspace id
|
|
21
|
+
from `key test` (`tenantId`) — never a secret.
|
|
22
|
+
|
|
23
|
+
Selection order: `--api-key` / `BUILDIFY_OPENAPI_API_KEY`, then `--profile` / `BUILDIFY_OPENAPI_PROFILE`,
|
|
24
|
+
then `.buildify-workspace`, then `[openapi].current`, then the only saved key. If more than one key
|
|
25
|
+
matches and none of those picked one, exit 2 with `data.reason=profile_required` and a list that has
|
|
26
|
+
no secrets. Agents show that list, ask once, then pass `--profile <name>` on later commands. Do **not**
|
|
27
|
+
run `config use` unless the user asked to change the default workspace. A pin with no matching local
|
|
28
|
+
key exits 2 with `data.reason=workspace_key_missing`.
|
|
29
|
+
|
|
30
|
+
**Do not** pass `--api-key` or the secret as an argv token (process list / shell history). Prefer
|
|
31
|
+
`buildify config add-profile <name>` (hidden prompt) or:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
printf '%s' 'keyId.secret' | buildify config add-profile acme
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`config set-key api_key` still writes the current profile, or the legacy `[openapi].api_key` when no
|
|
38
|
+
current profile is set. Never write the key into the project tree, a committed `.env`, issues, or chat
|
|
39
|
+
replies. After save, only display the masked value from `config show` or `config profiles`. A project
|
|
40
|
+
404 includes `tenantId` and `profile` so a miss can be a wrong workspace rather than a missing project.
|
|
41
|
+
|
|
42
|
+
**Out of scope:** creating OpenAPI keys. Credentials are created with `project create-credential` after
|
|
43
|
+
the user confirms project, name/label and form values. Publish with `workflow deploy` after the test run passes.
|
|
44
|
+
|
|
45
|
+
## Environment variables
|
|
46
|
+
|
|
47
|
+
| Variable | Meaning |
|
|
48
|
+
|----------|---------|
|
|
49
|
+
| `BUILDIFY_OPENAPI_BASE_URL` | API root (no trailing slash) |
|
|
50
|
+
| `BUILDIFY_OPENAPI_API_KEY` | Full Bearer secret `keyId.secret` (ignores profiles) |
|
|
51
|
+
| `BUILDIFY_OPENAPI_PROFILE` | Profile name; same as `--profile` |
|
|
52
|
+
| `BUILDIFY_OPENAPI_JSON` | If `1`/`true`/`yes`, same as global `--json` |
|
|
53
|
+
| `BUILDIFY_OPENAPI_NO_PROMPT` | If `1`/`true`/`yes`, never prompt for a missing key |
|
|
54
|
+
| `NO_COLOR` | Disable ANSI colors |
|
|
55
|
+
|
|
56
|
+
**Config file:** `~/.buildifyrc`, section `[openapi]`: `base_url`, legacy `api_key`, `current`, and
|
|
57
|
+
`profiles.<name>` (`api_key`, cached `tenant_id` / `key_name` / `key_prefix`). File mode `600`.
|
|
58
|
+
Shared with `buildify-publish`, which uses the top-level keys — the two do not collide.
|
|
59
|
+
|
|
60
|
+
**Precedence:** `--api-key` / `BUILDIFY_OPENAPI_API_KEY` > `--profile` / `BUILDIFY_OPENAPI_PROFILE` >
|
|
61
|
+
`.buildify-workspace` > `[openapi].current` > the only saved key > `profile_required` or `missing_api_key`.
|
|
62
|
+
Base URL: CLI flag > `BUILDIFY_OPENAPI_BASE_URL` > file > default.
|
|
63
|
+
|
|
64
|
+
## Exit codes
|
|
65
|
+
|
|
66
|
+
| Code | Meaning |
|
|
67
|
+
|------|---------|
|
|
68
|
+
| 0 | Success |
|
|
69
|
+
| 1 | Generic failure |
|
|
70
|
+
| 2 | Missing config/auth or HTTP 401/403 |
|
|
71
|
+
| 3 | Local validation (bad JSON, file not found) |
|
|
72
|
+
| 4 | HTTP 4xx/5xx, **or `workflow validate` found errors** |
|
|
73
|
+
| 5 | Network / timeout / connection error |
|
|
74
|
+
|
|
75
|
+
## Commands → HTTP
|
|
76
|
+
|
|
77
|
+
All paths are under `/openapi/v1`.
|
|
78
|
+
|
|
79
|
+
| CLI | Method | Path |
|
|
80
|
+
|-----|--------|------|
|
|
81
|
+
| `health` | GET | `/health` (no auth) |
|
|
82
|
+
| `key test` | GET | `/key/test` |
|
|
83
|
+
| `bundle list` | GET | `/bundles` |
|
|
84
|
+
| `bundle nodes` | GET | `/bundles/nodes` — flattened catalog (`icon`/`label`/`isTrigger` from `groups_json` + `nodes_json`) |
|
|
85
|
+
| `bundle node-properties` | GET | `/bundles/nodes/properties` |
|
|
86
|
+
| `bundle node-relations` | GET | `/bundles/nodes/relations` |
|
|
87
|
+
| `bundle doc` | GET | `/bundles/doc` |
|
|
88
|
+
| `bundle cred-types` | GET | `/bundles/credentials/types` |
|
|
89
|
+
| `bundle cred-properties` | GET | `/bundles/credentials/properties` |
|
|
90
|
+
| `project list` / `create` / `get` | GET / POST / GET | `/projects[...]` |
|
|
91
|
+
| `project credentials` | GET | `/projects/{id}/credentials` |
|
|
92
|
+
| `project create-credential` | POST | `/projects/{id}/credentials` |
|
|
93
|
+
| `project workers` | GET | `/projects/{id}/workers` |
|
|
94
|
+
| `workflow list` / `create` | GET / POST | `/projects/{id}/workflows` |
|
|
95
|
+
| `workflow get-draft` / `save-draft` | GET / POST | `/projects/{id}/workflows/{wid}/draft` |
|
|
96
|
+
| `workflow validate` | POST | `/projects/{id}/workflows/validate` |
|
|
97
|
+
| `workflow test-run` | POST | `/projects/{id}/workflows/test_runs` |
|
|
98
|
+
| `workflow cancel-test` | POST | `/projects/{id}/workflows/test_runs/{executionId}/cancel` |
|
|
99
|
+
| `workflow deploy` | POST | `/projects/{id}/workflows/{wid}/deploy` |
|
|
100
|
+
| `workflow published` | GET | `/projects/{id}/workflows/{wid}/published` |
|
|
101
|
+
| `workflow deployment` | GET | `/projects/{id}/workflows/{wid}/deployments/{deployId}` |
|
|
102
|
+
|
|
103
|
+
## Typical session
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 0. one-time config (prefer stdin; never put the secret on argv)
|
|
107
|
+
printf '%s' 'keyId.secret' | buildify config add-profile acme
|
|
108
|
+
buildify config set-key base_url https://openapi.buildify.cn
|
|
109
|
+
buildify --json key test
|
|
110
|
+
buildify --json config profiles # no secrets; ask the user if reason=profile_required
|
|
111
|
+
# missing key + --json → exit 2, data.reason=missing_api_key; ask the user, then save via stdin
|
|
112
|
+
|
|
113
|
+
# 1. where does this flow live, and which worker runs it
|
|
114
|
+
buildify --json project list
|
|
115
|
+
buildify --json project workers -p proj_xxx --online-only
|
|
116
|
+
|
|
117
|
+
# 2. what can I build with
|
|
118
|
+
buildify --json bundle list
|
|
119
|
+
buildify --json bundle nodes -b official/core
|
|
120
|
+
buildify --json bundle node-properties -b official/core -n WebhookTrigger
|
|
121
|
+
buildify --json bundle node-relations -b official/core -n WebhookTrigger
|
|
122
|
+
buildify bundle doc -b official/core # Markdown usage document
|
|
123
|
+
|
|
124
|
+
# 3. which credentials may the flow reference (secrets are never returned)
|
|
125
|
+
buildify --json project credentials -p proj_xxx
|
|
126
|
+
# missing name/label/data → exit 3 with defaults + form fields; show them, wait, then:
|
|
127
|
+
printf '%s' '{"host":"example.com","password":"…"}' | buildify --json project create-credential \
|
|
128
|
+
-p proj_xxx -b official/core --type Mysql --name mysql --label "生产只读" --file -
|
|
129
|
+
|
|
130
|
+
# 4. author flow.json locally, then loop until it is clean
|
|
131
|
+
buildify --json workflow validate -p proj_xxx -f ./flow.json
|
|
132
|
+
|
|
133
|
+
# 5. land it and try it
|
|
134
|
+
buildify --json workflow create -p proj_xxx -n "订单同步" --worker wk_xxx
|
|
135
|
+
buildify --json workflow save-draft -p proj_xxx -w wf_xxx -f ./flow.json
|
|
136
|
+
buildify --json workflow test-run -p proj_xxx -f ./flow.json --worker wk_xxx --timeout 60
|
|
137
|
+
buildify --json workflow deploy -p proj_xxx -w wf_xxx --worker wk_xxx \
|
|
138
|
+
--version-name "v1.0.0" --remark "首次上线" --wait
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`--file -` reads JSON from stdin (flow canvas or credential `data`).
|
|
142
|
+
|
|
143
|
+
## project create-credential
|
|
144
|
+
|
|
145
|
+
Requires `--project`. Also pass `--bundle` and `--type`. If `--name` / `--label` / credential
|
|
146
|
+
`--file` are omitted:
|
|
147
|
+
|
|
148
|
+
- **TTY:** show default name/label, prompt for form fields (password hidden), then confirm.
|
|
149
|
+
- **`--json`:** exit 3, `data.reason=credential_input_required`. `defaults` and `userMessage`
|
|
150
|
+
include the suggested name/label; `parameters` lists form fields (`secret: true` for secrets).
|
|
151
|
+
Agents must display those defaults, wait for confirmation and values, then create via stdin.
|
|
152
|
+
Never echo secrets; never pass `--data` on argv.
|
|
153
|
+
|
|
154
|
+
Response is metadata only (`name` / `label` / `type` / `credentialsId`). Duplicate
|
|
155
|
+
`bundle+type+name` in the same project returns HTTP 409.
|
|
156
|
+
|
|
157
|
+
## Version conflicts on save-draft
|
|
158
|
+
|
|
159
|
+
`workflow save-draft` reads the current draft to obtain the optimistic-lock `version` unless you pass
|
|
160
|
+
`--version`. If someone edited the flow in the console in between, the server returns a conflict
|
|
161
|
+
(exit 4) and the response `data` carries the latest draft — re-apply your changes on top of it
|
|
162
|
+
rather than retrying blindly.
|
|
163
|
+
|
|
164
|
+
## workflow validate
|
|
165
|
+
|
|
166
|
+
Checks, in this order: canvas structure, node existence in the workspace, `data.parameters`
|
|
167
|
+
against each node's `properties_json`, `edges[].data.relations[].name` against the source node's
|
|
168
|
+
`relations_json` (or the source node's custom `data.relations` for MCP), `data.credentials`
|
|
169
|
+
against the project's credentials, graph shape (trigger present, orphan nodes, cycles),
|
|
170
|
+
and WARNINGs if catalog `icon` is missing on a node or an edge relation is missing `label` /
|
|
171
|
+
custom `icon`/`_id`.
|
|
172
|
+
|
|
173
|
+
Response `data`:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"valid": false,
|
|
178
|
+
"errorCount": 1,
|
|
179
|
+
"warningCount": 2,
|
|
180
|
+
"issues": [
|
|
181
|
+
{"level": "ERROR", "nodeId": "n2", "path": "data.parameters.url", "message": "缺少必填参数: url"}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`valid: false` exits 4 even though HTTP is 200, so `until buildify workflow validate ...; do ...; done`
|
|
187
|
+
works as a loop condition.
|
|
188
|
+
|
|
189
|
+
## workflow test-run
|
|
190
|
+
|
|
191
|
+
Submits the flow to a worker and **blocks** until it finishes, then returns the aggregated result.
|
|
192
|
+
Node outputs are collected server-side from the execution event stream, so no WebSocket handling
|
|
193
|
+
is needed on the client.
|
|
194
|
+
|
|
195
|
+
Response `data`: `success`, `reason`, `executionId`, `nodes[]` (per node: `nodeId`, `status`,
|
|
196
|
+
`outputs[]` with `relation` + `payload`, `error`), and `events[]` in arrival order.
|
|
197
|
+
|
|
198
|
+
`--start-node` runs a single node instead of the whole graph. `--timeout` defaults to the server's
|
|
199
|
+
60s; the HTTP read timeout is set to `timeout + 30s`. On timeout the partial result is returned with
|
|
200
|
+
`reason: "timeout"` — use `workflow cancel-test` with the returned `executionId` to stop the worker.
|
|
201
|
+
|
|
202
|
+
## workflow deploy / published / deployment
|
|
203
|
+
|
|
204
|
+
`workflow deploy` promotes the **already saved draft** (omit `--file` after `save-draft`). `--worker` is
|
|
205
|
+
required and must already be bound to the flow. At publish time set:
|
|
206
|
+
|
|
207
|
+
- `--version-name` — version label written on this release (e.g. `v1.0.0`). Omit to let the server
|
|
208
|
+
generate `vyyyyMMdd-HHmmss`.
|
|
209
|
+
- `--remark` — release note on this version only (not the workflow's description). Omit for none.
|
|
210
|
+
|
|
211
|
+
Passing `--file` re-uploads canvas JSON and then needs `--version` or a draft fetch; don't do that
|
|
212
|
+
in the normal authoring loop.
|
|
213
|
+
|
|
214
|
+
Default `--wait` polls `GET .../deployments/{id}?statusOnly=true` (status + per-server result only)
|
|
215
|
+
until `deployStatus` is terminal, then does **one** full GET for the summary tables. Failures exit 4.
|
|
216
|
+
`--no-wait` returns immediately with `deploymentId` still `pending`.
|
|
217
|
+
|
|
218
|
+
Do **not** call `workflow published` right after a successful `--wait`; the wait response already has
|
|
219
|
+
`summary`. Use `workflow published` later to ask what is live.
|
|
220
|
+
|
|
221
|
+
`data.summary` (also returned by `workflow published`) is the human/agent report:
|
|
222
|
+
|
|
223
|
+
- `projectName`, `workflowName`, `versionName`, `publishRemark`
|
|
224
|
+
- overall `deployStatus` + `deployStatusIcon` (`✅`/`⚠️`/`❌`/`⏳`/…) + `deployStatusLabel`
|
|
225
|
+
- `servers[]`: `workerName`, `remark`, `isOnline`, `onlineIcon` (`🟢`/`⚫`), `onlineLabel`,
|
|
226
|
+
`deployStatus`, `deployStatusIcon`, `deployStatusLabel`, `message`
|
|
227
|
+
- `nodes[]`: `label` / `summary` / `isTrigger` / `webhookPath`(JSON 仍返回;人读报告不打印节点表)
|
|
228
|
+
- `credentials[]`: `bundleName` / `type` / `name` / `label` (never secrets)
|
|
229
|
+
- `bundles[]`
|
|
230
|
+
|
|
231
|
+
Without `--json`, `workflow deploy` / `published` / `deployment` print Markdown tables with those icons
|
|
232
|
+
(项目 / 服务器 / 凭证;凭证表含 `name` 与 `label`,不含节点表)。
|
|
233
|
+
With `--json`, agents should render the same tables (see buildify-flow-dev skill).
|
|
234
|
+
|
|
235
|
+
`workflow published` is the live snapshot after deploy. `workflow deployment -d deploy_…` is one attempt.
|
|
236
|
+
|
|
237
|
+
## JSON output schema
|
|
238
|
+
|
|
239
|
+
With `--json`, stdout is exactly one JSON object per invocation:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{"ok": true, "code": 200, "msg": "...", "http_status": 200, "data": {}}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`data` matches the server `R.data` field.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buildify-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI for the Buildify open API: discover bundle nodes, author and test workflows (machine Bearer auth)
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx<1,>=0.27
|
|
8
|
+
Requires-Dist: typer<1,>=0.12
|
|
9
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
10
|
+
Requires-Dist: tomli-w<2,>=1.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# buildify-cli
|
|
15
|
+
|
|
16
|
+
CLI for the Buildify **open API** (`buildify-open-api-server`): discover installed bundle nodes,
|
|
17
|
+
read their form schemas and documentation, reference workspace credentials, and create, validate,
|
|
18
|
+
test run and deploy workflows — all with a machine API key, no browser session.
|
|
19
|
+
|
|
20
|
+
`workflow deploy` publishes a draft to workers that are already bound to the flow. It does not pick
|
|
21
|
+
servers. Credentials can be created with `project create-credential` after the user confirms
|
|
22
|
+
the project, name/label and form values.
|
|
23
|
+
|
|
24
|
+
For publishing bundle JARs see the sibling [buildify-publish-cli](../buildify-publish-cli).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install buildify-cli # or pipx / uv tool install
|
|
30
|
+
buildify help # full Markdown reference on stdout
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python 3.10+.
|
|
34
|
+
|
|
35
|
+
## Configure
|
|
36
|
+
|
|
37
|
+
Create a key in the console (workspace settings → Open API keys). The full `keyId.secret` is shown
|
|
38
|
+
**only once**. One key belongs to one workspace. Save each workspace under its own profile:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
printf '%s' 'keyId.secret' | buildify config add-profile acme
|
|
42
|
+
buildify config set-key base_url https://openapi.buildify.cn
|
|
43
|
+
buildify --json key test
|
|
44
|
+
buildify --json config profiles
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`add-profile` does not change the default. `config use <name>` does, for a person at the terminal.
|
|
48
|
+
Agents should pass `--profile <name>` instead. A repo may commit `.buildify-workspace` with only the
|
|
49
|
+
public workspace id (`tenantId` from `key test`).
|
|
50
|
+
|
|
51
|
+
If a command runs with no key: a TTY prompts and saves locally; `--json` / non-TTY exits 2 with
|
|
52
|
+
`data.reason=missing_api_key`. Several saved keys and no selection exits 2 with
|
|
53
|
+
`data.reason=profile_required`.
|
|
54
|
+
|
|
55
|
+
Config lives in the `[openapi]` section of `~/.buildifyrc` (mode `600`). Do not pass `--api-key` on
|
|
56
|
+
the command line, and do not commit the rc file or paste the secret into the repo.
|
|
57
|
+
`BUILDIFY_OPENAPI_BASE_URL` and `BUILDIFY_OPENAPI_API_KEY` override the file at runtime.
|
|
58
|
+
`BUILDIFY_OPENAPI_PROFILE` selects a saved profile. `BUILDIFY_OPENAPI_NO_PROMPT=1` disables the
|
|
59
|
+
interactive prompt. `config set-key api_key` still updates the current profile, or the legacy single key.
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
| Group | Commands |
|
|
64
|
+
|-------|----------|
|
|
65
|
+
| — | `health`, `version`, `help` |
|
|
66
|
+
| `key` | `test` |
|
|
67
|
+
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
68
|
+
| `project` | `list`, `create`, `get`, `credentials`, `create-credential`, `workers` |
|
|
69
|
+
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployment` |
|
|
70
|
+
| `config` | `init`, `set-key`, `add-profile`, `use`, `profiles`, `remove-profile`, `show` |
|
|
71
|
+
|
|
72
|
+
Every command accepts `--json` for a single-line envelope
|
|
73
|
+
(`{ok, code, msg, http_status, data}`) and maps failures onto stable exit codes:
|
|
74
|
+
`0` ok, `1` generic, `2` auth/config, `3` local validation, `4` HTTP 4xx/5xx or validation errors,
|
|
75
|
+
`5` network.
|
|
76
|
+
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
buildify --json bundle nodes -b official/core
|
|
81
|
+
buildify --json bundle node-properties -b official/core -n HttpRequestNode
|
|
82
|
+
buildify --json workflow validate -p proj_xxx -f ./flow.json
|
|
83
|
+
buildify --json workflow test-run -p proj_xxx -f ./flow.json --worker wk_xxx
|
|
84
|
+
buildify --json workflow deploy -p proj_xxx -w wf_xxx --worker wk_xxx --wait
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv venv && uv pip install -e '.[dev]'
|
|
91
|
+
python -m buildify_cli --help
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`src/buildify_cli/usage.md` is the source of truth for `buildify help`; keep this README aligned
|
|
95
|
+
for humans.
|