@flowweave-ai/cli 0.1.0
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 +27 -0
- package/bin/flowweave.mjs +376 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @flowweave-ai/cli
|
|
2
|
+
|
|
3
|
+
`@flowweave-ai/cli` 是可独立安装的 FlowWeave 平台命令行客户端,要求 Node.js 22 或更高版本。包中不包含 Python、Docker 或平台源码依赖。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @flowweave-ai/cli
|
|
7
|
+
flowweave config init --base-url https://host.example/flowweave
|
|
8
|
+
flowweave health --ready
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
配置只保存基础 URL 到 `~/.config/flowweave/config.json`(可由 `FLOWWEAVE_CONFIG_PATH` 覆盖)。当前平台不需要登录。
|
|
12
|
+
|
|
13
|
+
页面域命令包括 `node`、`node-directory`、`capability`、`environment`、`flow`、`run`、`model` 和 `agent`。它们分别覆盖节点资产、能力仓库、终端环境、流程编排、FlowRun、大模型配置与 Agent 工作台的常用原子操作。每个命令的 JSON 请求体与在线 OpenAPI 一致;运行 `flowweave <域> --help` 查看映射。
|
|
14
|
+
|
|
15
|
+
常用命令示例:
|
|
16
|
+
|
|
17
|
+
`flowweave node create --data-file ./node.json`、`flowweave capability import --type SKILL --file ./skill.zip`、`flowweave environment create --data '{"name":"Python"}'`、`flowweave flow create --data-file ./flow.json`、`flowweave run start --flow <flow-id> --environment-version <version-id>`。
|
|
18
|
+
|
|
19
|
+
`api`、`upload`、`ws` 是完整契约入口:任意当前或未来的 REST、multipart、WebSocket 原子接口均可直接调用,不需要等待 CLI 发布。`ws` 使用 Node 原生 WebSocket;当前公开 FlowWeave WebSocket 接口无需自定义请求头。写操作支持 `--dry-run`,并可使用 `-H 'Idempotency-Key: …'` 传入一次性幂等键。对于没有快捷命令的新接口,先运行 `flowweave openapi --paths`,再通过通用命令调用。
|
|
20
|
+
|
|
21
|
+
安装页面域 skill:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx skills add ZME7777777/FlowWeave -g -y --full-depth
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
仓库尚未发布该 npm 包前,进入 `packages/cli` 后运行 `npm pack` 生成 tarball,并以 `npm install -g ./flowweave-ai-cli-*.tgz` 安装。发布到 npm registry 需要拥有 `@flowweave-ai` scope 的发布权限;本仓库不会自动发布。
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
const API_PREFIX = '/api/v1';
|
|
8
|
+
const CONFIG_PATH = process.env.FLOWWEAVE_CONFIG_PATH
|
|
9
|
+
|| `${process.env.XDG_CONFIG_HOME || `${homedir()}/.config`}/flowweave/config.json`;
|
|
10
|
+
|
|
11
|
+
class CliError extends Error {}
|
|
12
|
+
|
|
13
|
+
function usage() {
|
|
14
|
+
return `用法:flowweave <命令> [选项]
|
|
15
|
+
|
|
16
|
+
配置与发现:
|
|
17
|
+
config init --base-url <URL> [--force] 设置平台基础 URL(无需登录)
|
|
18
|
+
config show 显示当前配置
|
|
19
|
+
health [--ready] 健康检查
|
|
20
|
+
openapi [--paths] 查看在线 OpenAPI 契约
|
|
21
|
+
|
|
22
|
+
通用完整接口:
|
|
23
|
+
api <get|post|put|patch|delete> <PATH> [--data JSON|--data-file FILE]
|
|
24
|
+
upload <post|put|patch> <PATH> --file name=FILE [--form name=value]
|
|
25
|
+
ws <PATH> [--message TEXT|--message-json JSON] [--max-messages N]
|
|
26
|
+
|
|
27
|
+
页面域原子操作:
|
|
28
|
+
node <list|get|create|update|delete> [ID] [--data JSON|--data-file FILE]
|
|
29
|
+
node-directory <list|create> [--data JSON|--data-file FILE]
|
|
30
|
+
capability <list|validate|commit|import> ...
|
|
31
|
+
environment <list|get|create|update|delete|setup|publish|stop|version-delete> ...
|
|
32
|
+
flow <list|get|create|update|validate|delete> ...
|
|
33
|
+
run <list|get|start|delete|runtime|replace|cancel|complete|events> ...
|
|
34
|
+
model <list|create|update|delete|discover|test|oauth-start|oauth-poll|oauth-status|oauth-revoke> ...
|
|
35
|
+
agent <default|workspace|runtime|conversations|conversation|create|send|interrupt|resume> ...
|
|
36
|
+
|
|
37
|
+
所有写入操作都可加 --dry-run 仅查看最终请求。运行 flowweave <命令> --help 查看该命令说明。`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function optionValues(args, name) {
|
|
41
|
+
const values = [];
|
|
42
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
43
|
+
if (args[index] === name) {
|
|
44
|
+
if (index + 1 >= args.length) throw new CliError(`${name} 缺少值`);
|
|
45
|
+
values.push(args[index + 1]);
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function option(args, name) {
|
|
53
|
+
return optionValues(args, name).at(-1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function flag(args, name) {
|
|
57
|
+
return args.includes(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function positional(args) {
|
|
61
|
+
const values = [];
|
|
62
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
63
|
+
if (args[index].startsWith('--') || args[index] === '-H' || args[index] === '-q') {
|
|
64
|
+
if (!['--dry-run', '--raw', '--ready', '--paths', '--force'].includes(args[index])) index += 1;
|
|
65
|
+
} else {
|
|
66
|
+
values.push(args[index]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return values;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeBaseUrl(value) {
|
|
73
|
+
let url;
|
|
74
|
+
try { url = new URL(value.trim()); } catch { throw new CliError('--base-url 必须是绝对 HTTP(S) URL'); }
|
|
75
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
76
|
+
throw new CliError('--base-url 只能是无凭据、无查询参数、无片段的 HTTP(S) URL');
|
|
77
|
+
}
|
|
78
|
+
return url.toString().replace(/\/$/, '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadConfig() {
|
|
82
|
+
let data;
|
|
83
|
+
try { data = JSON.parse(await readFile(CONFIG_PATH, 'utf8')); } catch {
|
|
84
|
+
throw new CliError(`尚未配置 FlowWeave。请执行:flowweave config init --base-url <URL>(${CONFIG_PATH})`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof data?.base_url !== 'string') throw new CliError(`配置 ${CONFIG_PATH} 未定义 base_url`);
|
|
87
|
+
return { baseUrl: normalizeBaseUrl(data.base_url) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function saveConfig(baseUrl, force) {
|
|
91
|
+
try { await readFile(CONFIG_PATH); if (!force) throw new CliError(`配置已存在于 ${CONFIG_PATH};传入 --force 可覆盖`); }
|
|
92
|
+
catch (error) { if (error instanceof CliError) throw error; }
|
|
93
|
+
const value = normalizeBaseUrl(baseUrl);
|
|
94
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
95
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify({ base_url: value }, null, 2)}\n`, { mode: 0o600 });
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseJson(value, source) {
|
|
100
|
+
try { return JSON.parse(value); } catch (error) { throw new CliError(`${source} 必须是合法 JSON:${error.message}`); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function payload(args) {
|
|
104
|
+
const inline = option(args, '--data');
|
|
105
|
+
const file = option(args, '--data-file');
|
|
106
|
+
if (inline && file) throw new CliError('--data 与 --data-file 只能使用其中一个');
|
|
107
|
+
if (file) return parseJson(await readFile(resolve(file), 'utf8'), '--data-file');
|
|
108
|
+
return inline ? parseJson(inline, '--data') : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pairs(values, separator, label) {
|
|
112
|
+
return values.map((value) => {
|
|
113
|
+
const index = value.indexOf(separator);
|
|
114
|
+
if (index <= 0) throw new CliError(`${label} 必须使用 name${separator}value 形式`);
|
|
115
|
+
return [value.slice(0, index), value.slice(index + 1)];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pathUrl(baseUrl, path, { raw = false, query = [] } = {}) {
|
|
120
|
+
if (!path.startsWith('/') || /^https?:/i.test(path) || path.includes('#')) {
|
|
121
|
+
throw new CliError('PATH 必须是以 / 开头的相对平台路径,不能是完整 URL');
|
|
122
|
+
}
|
|
123
|
+
const url = new URL(baseUrl);
|
|
124
|
+
const apiPath = raw || path.startsWith(`${API_PREFIX}/`) ? path : `${API_PREFIX}${path}`;
|
|
125
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}${apiPath}`;
|
|
126
|
+
for (const [name, value] of query) url.searchParams.append(name, value);
|
|
127
|
+
return url;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function headers(args) { return Object.fromEntries(pairs(optionValues(args, '-H').concat(optionValues(args, '--header')), ':', '--header').map(([name, value]) => [name.trim(), value.trim()])); }
|
|
131
|
+
function queries(args) { return pairs(optionValues(args, '-q').concat(optionValues(args, '--query')), '=', '--query'); }
|
|
132
|
+
|
|
133
|
+
async function request(method, path, args, { raw = false, body, form } = {}) {
|
|
134
|
+
const { baseUrl } = await loadConfig();
|
|
135
|
+
const url = pathUrl(baseUrl, path, { raw, query: queries(args) });
|
|
136
|
+
const requestBody = body === undefined ? await payload(args) : body;
|
|
137
|
+
if (flag(args, '--dry-run')) return { method, url: url.toString(), payload: requestBody ?? form ?? null };
|
|
138
|
+
const requestHeaders = { Accept: 'application/json', ...headers(args) };
|
|
139
|
+
let encoded;
|
|
140
|
+
if (form) { encoded = form; } else if (requestBody !== undefined) { encoded = JSON.stringify(requestBody); requestHeaders['Content-Type'] ||= 'application/json'; }
|
|
141
|
+
let response;
|
|
142
|
+
try { response = await fetch(url, { method, headers: requestHeaders, body: encoded }); } catch (error) { throw new CliError(`无法连接 FlowWeave ${url}:${error.message}`); }
|
|
143
|
+
const text = await response.text();
|
|
144
|
+
let value;
|
|
145
|
+
try { value = text ? JSON.parse(text) : { status: response.status }; } catch { value = { status: response.status, body: text }; }
|
|
146
|
+
if (!response.ok) throw new CliError(`HTTP ${response.status}: ${JSON.stringify(value)}`);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function objectPayload(args, defaults = {}) {
|
|
151
|
+
const value = await payload(args);
|
|
152
|
+
if (value === undefined) return defaults;
|
|
153
|
+
if (value === null || Array.isArray(value) || typeof value !== 'object') {
|
|
154
|
+
throw new CliError('--data 请求体必须是 JSON 对象');
|
|
155
|
+
}
|
|
156
|
+
return { ...value, ...defaults };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function openapiPaths(document) {
|
|
160
|
+
if (!document || typeof document !== 'object' || !document.paths || typeof document.paths !== 'object') {
|
|
161
|
+
throw new CliError('OpenAPI 文档不包含 paths 对象');
|
|
162
|
+
}
|
|
163
|
+
const methods = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
164
|
+
return Object.entries(document.paths)
|
|
165
|
+
.flatMap(([path, operations]) => (operations && typeof operations === 'object'
|
|
166
|
+
? Object.keys(operations)
|
|
167
|
+
.filter((method) => methods.has(method.toLowerCase()))
|
|
168
|
+
.map((method) => ({ method: method.toUpperCase(), path }))
|
|
169
|
+
: []))
|
|
170
|
+
.sort((left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function print(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); }
|
|
174
|
+
|
|
175
|
+
const resourceRoutes = {
|
|
176
|
+
node: '/node-assets',
|
|
177
|
+
'node-directory': '/node-directories',
|
|
178
|
+
flow: '/flows',
|
|
179
|
+
environment: '/terminal-environments',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
async function crud(command, args) {
|
|
183
|
+
const [action, id] = positional(args);
|
|
184
|
+
const route = resourceRoutes[command];
|
|
185
|
+
const methods = { list: 'GET', get: 'GET', create: 'POST', update: 'PUT', delete: 'DELETE' };
|
|
186
|
+
if (!methods[action]) throw new CliError(`${command} 支持 list|get|create|update|delete`);
|
|
187
|
+
if (['get', 'update', 'delete'].includes(action) && !id) throw new CliError(`${command} ${action} 必须提供 ID`);
|
|
188
|
+
if (['list', 'create'].includes(action) && id) throw new CliError(`${command} ${action} 不接受 ID`);
|
|
189
|
+
return request(methods[action], id ? `${route}/${id}` : route, args);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function capability(args) {
|
|
193
|
+
const [action, id] = positional(args);
|
|
194
|
+
if (action === 'list') return request('GET', '/capabilities', args);
|
|
195
|
+
if (action === 'validate' || action === 'import') {
|
|
196
|
+
const type = option(args, '--type');
|
|
197
|
+
const source = option(args, '--file');
|
|
198
|
+
if (!type || !source) throw new CliError(`capability ${action} 需要 --type 和 --file`);
|
|
199
|
+
const file = resolve(source);
|
|
200
|
+
const content = await readFile(file);
|
|
201
|
+
const body = { capability_type: type, filename: basename(file), content_base64: content.toString('base64') };
|
|
202
|
+
const validated = await request('POST', '/capability-imports/validate', args, { body });
|
|
203
|
+
if (action === 'validate' || flag(args, '--dry-run')) return validated;
|
|
204
|
+
return request('POST', '/capability-imports', args, { body: { import_token: validated.import_token } });
|
|
205
|
+
}
|
|
206
|
+
if (action === 'commit') {
|
|
207
|
+
const token = option(args, '--import-token') || id;
|
|
208
|
+
if (!token) throw new CliError('capability commit 需要 --import-token');
|
|
209
|
+
return request('POST', '/capability-imports', args, { body: { import_token: token } });
|
|
210
|
+
}
|
|
211
|
+
throw new CliError('capability 支持 list|validate|commit|import');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function environment(args) {
|
|
215
|
+
const [action, id] = positional(args);
|
|
216
|
+
if (['list', 'get', 'create', 'update', 'delete'].includes(action)) return crud('environment', args);
|
|
217
|
+
if (action === 'setup') { if (!id) throw new CliError('environment setup 需要环境 ID'); return request('POST', `/terminal-environments/${id}/setup-sessions`, args, { body: await objectPayload(args) }); }
|
|
218
|
+
if (action === 'publish') { if (!id) throw new CliError('environment publish 需要 Setup Session ID'); return request('POST', `/environment-setup-sessions/${id}/publish`, args, { body: {} }); }
|
|
219
|
+
if (action === 'stop') { if (!id) throw new CliError('environment stop 需要 Setup Session ID'); return request('DELETE', `/environment-setup-sessions/${id}`, args); }
|
|
220
|
+
if (action === 'version-delete') {
|
|
221
|
+
const versionId = option(args, '--version');
|
|
222
|
+
if (!id || !versionId) throw new CliError('environment version-delete 需要环境 ID 和 --version');
|
|
223
|
+
return request('DELETE', `/terminal-environments/${id}/versions/${versionId}`, args);
|
|
224
|
+
}
|
|
225
|
+
throw new CliError('environment 支持 list|get|create|update|delete|setup|publish|stop|version-delete');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function flow(args) {
|
|
229
|
+
const [action, id] = positional(args);
|
|
230
|
+
if (['list', 'get', 'create', 'update', 'delete'].includes(action)) return crud('flow', args);
|
|
231
|
+
if (action === 'validate') { if (!id) throw new CliError('flow validate 需要流程 ID'); return request('POST', `/flows/${id}/validate`, args, { body: {} }); }
|
|
232
|
+
throw new CliError('flow 支持 list|get|create|update|validate|delete');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function run(args) {
|
|
236
|
+
const [action, id] = positional(args);
|
|
237
|
+
if (action === 'list') return request('GET', '/flow-runs', args);
|
|
238
|
+
if (action === 'start') {
|
|
239
|
+
const flowId = option(args, '--flow'); const environmentVersion = option(args, '--environment-version');
|
|
240
|
+
if (!flowId || !environmentVersion) throw new CliError('run start 需要 --flow 和 --environment-version');
|
|
241
|
+
const body = await objectPayload(args, {
|
|
242
|
+
environment_version_id: environmentVersion,
|
|
243
|
+
...(option(args, '--name') ? { name: option(args, '--name') } : {}),
|
|
244
|
+
});
|
|
245
|
+
return request('POST', `/flows/${flowId}/runs`, args, { body });
|
|
246
|
+
}
|
|
247
|
+
if (!id) throw new CliError(`run ${action || ''} 需要 FlowRun ID`);
|
|
248
|
+
if (action === 'get') return request('GET', `/flow-runs/${id}`, args);
|
|
249
|
+
if (action === 'delete') return request('DELETE', `/flow-runs/${id}`, args);
|
|
250
|
+
if (action === 'runtime') return request('GET', `/flow-runs/${id}/runtime`, args);
|
|
251
|
+
if (action === 'replace') return request('POST', `/flow-runs/${id}/runtime/replacements`, args);
|
|
252
|
+
if (action === 'cancel') return request('POST', `/flow-runs/${id}/cancel`, args, { body: {} });
|
|
253
|
+
if (action === 'complete') return request('POST', `/flow-runs/${id}/complete`, args, { body: {} });
|
|
254
|
+
if (action === 'events') return request('GET', `/flow-runs/${id}/events`, args);
|
|
255
|
+
throw new CliError('run 支持 list|get|start|delete|runtime|replace|cancel|complete|events');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function model(args) {
|
|
259
|
+
const [action, id] = positional(args);
|
|
260
|
+
if (action === 'list') return request('GET', '/model-providers', args);
|
|
261
|
+
if (action === 'create') return request('POST', '/model-providers', args);
|
|
262
|
+
if (action === 'discover') {
|
|
263
|
+
return id
|
|
264
|
+
? request('POST', `/model-providers/${id}/discover-models`, args)
|
|
265
|
+
: request('POST', '/model-providers/discover-models', args);
|
|
266
|
+
}
|
|
267
|
+
if (!id) throw new CliError(`model ${action || ''} 需要模型供应商 ID`);
|
|
268
|
+
if (action === 'update') return request('PUT', `/model-providers/${id}`, args);
|
|
269
|
+
if (action === 'delete') return request('DELETE', `/model-providers/${id}`, args);
|
|
270
|
+
if (action === 'test') return request('POST', `/model-providers/${id}/test`, args);
|
|
271
|
+
if (action === 'oauth-start') return request('POST', `/model-providers/${id}/oauth/device/start`, args, { body: await objectPayload(args) });
|
|
272
|
+
if (action === 'oauth-poll') return request('POST', `/model-providers/${id}/oauth/device/poll`, args, { body: await objectPayload(args) });
|
|
273
|
+
if (action === 'oauth-status') return request('GET', `/model-providers/${id}/oauth/status`, args);
|
|
274
|
+
if (action === 'oauth-revoke') return request('DELETE', `/model-providers/${id}/oauth`, args);
|
|
275
|
+
throw new CliError('model 支持 list|create|update|delete|discover|test|oauth-start|oauth-poll|oauth-status|oauth-revoke');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function agent(args) {
|
|
279
|
+
const [action, workspace, binding] = positional(args);
|
|
280
|
+
if (action === 'default') return request('GET', '/agent-workspaces/default', args);
|
|
281
|
+
if (!workspace) throw new CliError(`agent ${action || ''} 需要 workspace ID`);
|
|
282
|
+
const base = `/agent-workspaces/${workspace}`;
|
|
283
|
+
if (action === 'workspace') return request('GET', base, args);
|
|
284
|
+
if (action === 'runtime') return request('GET', `${base}/runtime`, args);
|
|
285
|
+
if (action === 'conversations') return request('GET', `${base}/conversations`, args);
|
|
286
|
+
if (action === 'create') return request('POST', `${base}/conversations`, args);
|
|
287
|
+
if (!binding) throw new CliError(`agent ${action || ''} 需要会话 binding ID`);
|
|
288
|
+
if (action === 'conversation') return request('GET', `${base}/conversations/${binding}`, args);
|
|
289
|
+
if (action === 'send') return request('POST', `${base}/conversations/${binding}/messages`, args);
|
|
290
|
+
if (action === 'interrupt') return request('POST', `${base}/conversations/${binding}/interrupt`, args, { body: {} });
|
|
291
|
+
if (action === 'resume') return request('POST', `${base}/conversations/${binding}/resume`, args, { body: {} });
|
|
292
|
+
throw new CliError('agent 支持 default|workspace|runtime|conversations|conversation|create|send|interrupt|resume');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function upload(args) {
|
|
296
|
+
const [method, path] = positional(args);
|
|
297
|
+
if (!['post', 'put', 'patch'].includes(method) || !path) throw new CliError('upload 用法:upload <post|put|patch> <PATH> --file name=FILE');
|
|
298
|
+
const fields = pairs(optionValues(args, '--form'), '=', '--form');
|
|
299
|
+
const files = pairs(optionValues(args, '--file'), '=', '--file');
|
|
300
|
+
if (!fields.length && !files.length) throw new CliError('upload 至少需要一个 --form 或 --file 参数');
|
|
301
|
+
const summary = {
|
|
302
|
+
fields: Object.fromEntries(fields),
|
|
303
|
+
files: files.map(([name, file]) => ({ field: name, path: resolve(file), filename: basename(file) })),
|
|
304
|
+
};
|
|
305
|
+
if (flag(args, '--dry-run')) return request(method.toUpperCase(), path, args, { body: summary });
|
|
306
|
+
const form = new FormData();
|
|
307
|
+
for (const [name, value] of fields) form.append(name, value);
|
|
308
|
+
for (const [name, file] of files) form.append(name, new Blob([await readFile(resolve(file))]), basename(file));
|
|
309
|
+
return request(method.toUpperCase(), path, args, { form });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function websocket(args) {
|
|
313
|
+
const [path] = positional(args);
|
|
314
|
+
if (!path) throw new CliError('ws 需要 PATH');
|
|
315
|
+
if (headers(args) && Object.keys(headers(args)).length) {
|
|
316
|
+
throw new CliError('ws 暂不支持自定义请求头;当前 FlowWeave WebSocket 接口无需此项');
|
|
317
|
+
}
|
|
318
|
+
const { baseUrl } = await loadConfig();
|
|
319
|
+
const url = pathUrl(baseUrl, path, { query: queries(args) });
|
|
320
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
321
|
+
const maxMessages = Number(option(args, '--max-messages') || 0);
|
|
322
|
+
if (!Number.isInteger(maxMessages) || maxMessages < 0) throw new CliError('--max-messages 必须是大于或等于零的整数');
|
|
323
|
+
let message = option(args, '--message');
|
|
324
|
+
const messageJson = option(args, '--message-json');
|
|
325
|
+
if (message && messageJson) throw new CliError('--message 与 --message-json 只能使用其中一个');
|
|
326
|
+
if (messageJson) message = JSON.stringify(parseJson(messageJson, '--message-json'));
|
|
327
|
+
if (flag(args, '--dry-run')) return { url: url.toString(), message: message || null, max_messages: maxMessages };
|
|
328
|
+
await new Promise((resolve, reject) => {
|
|
329
|
+
let received = 0;
|
|
330
|
+
let settled = false;
|
|
331
|
+
const finish = (error) => {
|
|
332
|
+
if (settled) return;
|
|
333
|
+
settled = true;
|
|
334
|
+
if (error) reject(error); else resolve();
|
|
335
|
+
};
|
|
336
|
+
const socket = new WebSocket(url);
|
|
337
|
+
socket.addEventListener('open', () => { if (message) socket.send(message); });
|
|
338
|
+
socket.addEventListener('message', (event) => {
|
|
339
|
+
const text = String(event.data);
|
|
340
|
+
try { print(JSON.parse(text)); } catch { process.stdout.write(`${text}\n`); }
|
|
341
|
+
received += 1;
|
|
342
|
+
if (maxMessages && received >= maxMessages) socket.close(1000, 'message limit reached');
|
|
343
|
+
});
|
|
344
|
+
socket.addEventListener('error', () => finish(new CliError(`无法连接 FlowWeave WebSocket ${url}`)));
|
|
345
|
+
socket.addEventListener('close', () => finish());
|
|
346
|
+
process.once('SIGINT', () => socket.close(1000, 'interrupted'));
|
|
347
|
+
});
|
|
348
|
+
return { status: 'closed', messages: maxMessages || undefined };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function main(argv) {
|
|
352
|
+
const [command, ...args] = argv;
|
|
353
|
+
if (!command || flag(args, '--help') || command === '--help') { process.stdout.write(`${usage()}\n`); return; }
|
|
354
|
+
let result;
|
|
355
|
+
if (command === 'config') {
|
|
356
|
+
const [action] = positional(args);
|
|
357
|
+
if (action === 'init') { const baseUrl = option(args, '--base-url'); if (!baseUrl) throw new CliError('config init 需要 --base-url'); result = { config_path: CONFIG_PATH, base_url: await saveConfig(baseUrl, flag(args, '--force')) }; }
|
|
358
|
+
else if (action === 'show') { const { baseUrl } = await loadConfig(); result = { config_path: CONFIG_PATH, base_url: baseUrl }; }
|
|
359
|
+
else throw new CliError('config 支持 init|show');
|
|
360
|
+
} else if (command === 'health') result = request('GET', flag(args, '--ready') ? '/health/ready' : '/health', args, { raw: true });
|
|
361
|
+
else if (command === 'openapi') { result = request('GET', '/openapi.json', args, { raw: true }); if (flag(args, '--paths')) result = openapiPaths(await result); }
|
|
362
|
+
else if (command === 'api') { const [method, path] = positional(args); if (!method || !path) throw new CliError('api 用法:api <method> <PATH>'); result = request(method.toUpperCase(), path, args, { raw: flag(args, '--raw') }); }
|
|
363
|
+
else if (command === 'upload') result = upload(args);
|
|
364
|
+
else if (command === 'ws') result = websocket(args);
|
|
365
|
+
else if (['node', 'node-directory'].includes(command)) result = crud(command, args);
|
|
366
|
+
else if (command === 'capability') result = capability(args);
|
|
367
|
+
else if (command === 'environment') result = environment(args);
|
|
368
|
+
else if (command === 'flow') result = flow(args);
|
|
369
|
+
else if (command === 'run') result = run(args);
|
|
370
|
+
else if (command === 'model') result = model(args);
|
|
371
|
+
else if (command === 'agent') result = agent(args);
|
|
372
|
+
else throw new CliError(`未知命令:${command}`);
|
|
373
|
+
print(await result);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
main(process.argv.slice(2)).catch((error) => { process.stderr.write(`flowweave: error: ${error.message}\n`); process.exitCode = 2; });
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowweave-ai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FlowWeave 平台命令行客户端",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flowweave": "bin/flowweave.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.mjs",
|
|
18
|
+
"lint": "node --check bin/flowweave.mjs",
|
|
19
|
+
"typecheck": "node --check bin/flowweave.mjs"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|