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.
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ """Stable exit codes for scripts and AI agents (same scheme as buildify-publish)."""
2
+
3
+ OK = 0
4
+ GENERIC = 1
5
+ AUTH_CONFIG = 2
6
+ LOCAL_VALIDATION = 3
7
+ SERVER_CLIENT_ERROR = 4
8
+ NETWORK = 5
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.