@mindscraft/branch-video-agent-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 +76 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/commands/production.d.ts +2 -0
- package/dist/commands/production.js +75 -0
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +39 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +26 -0
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +102 -0
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.js +14 -0
- package/dist/lib/flags.d.ts +24 -0
- package/dist/lib/flags.js +102 -0
- package/dist/lib/http.d.ts +18 -0
- package/dist/lib/http.js +73 -0
- package/dist/lib/payload.d.ts +7 -0
- package/dist/lib/payload.js +49 -0
- package/dist/lib/runtime.d.ts +11 -0
- package/dist/lib/runtime.js +26 -0
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/types.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Branch Video Agent CLI
|
|
2
|
+
|
|
3
|
+
`@mindscraft/branch-video-agent-cli` 是一个可脱离当前代码仓库使用的发布版 CLI,用于通过 Bearer agent token 调用 branch-video 与 AIHub agent API。
|
|
4
|
+
|
|
5
|
+
## 安装与执行
|
|
6
|
+
|
|
7
|
+
推荐直接使用 `npx`:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
npx -y @mindscraft/branch-video-agent-cli --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
中国大陆用户如果希望更稳定、更快地安装,可以走 npm 的公开镜像:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
npx -y --registry=https://registry.npmmirror.com @mindscraft/branch-video-agent-cli --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
也可以全局安装:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
npm install -g @mindscraft/branch-video-agent-cli
|
|
23
|
+
branch-video-agent --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
环境变量:
|
|
27
|
+
|
|
28
|
+
- `AIHUB_AGENT_TOKEN`
|
|
29
|
+
- `BRANCH_VIDEO_AGENT_BASE_URL`
|
|
30
|
+
|
|
31
|
+
最小调用示例:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
@'
|
|
35
|
+
{
|
|
36
|
+
"filter": "mine",
|
|
37
|
+
"page": 1,
|
|
38
|
+
"limit": 20
|
|
39
|
+
}
|
|
40
|
+
'@ | npx -y @mindscraft/branch-video-agent-cli project list
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 在仓库内构建发布包
|
|
44
|
+
|
|
45
|
+
从仓库根目录执行:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
npm run agent:cli:package:build
|
|
49
|
+
npm run agent:cli:package:smoke
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
产物输出到:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
packages/branch-video-agent-cli/dist/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 发布步骤
|
|
59
|
+
|
|
60
|
+
从仓库根目录先完成构建与 smoke check,然后进入子包目录执行发布:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
npm run agent:cli:package:build
|
|
64
|
+
npm run agent:cli:package:smoke
|
|
65
|
+
Set-Location packages/branch-video-agent-cli
|
|
66
|
+
npm login --registry=https://registry.npmjs.org/
|
|
67
|
+
npm pack
|
|
68
|
+
npm publish
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
当前子包已经固定为发布到 `https://registry.npmjs.org/`,不会沿用仓库根目录的内网 `.npmrc`。
|
|
72
|
+
|
|
73
|
+
发布前请先确认两件事:
|
|
74
|
+
|
|
75
|
+
1. 你在 npmjs 上拥有当前包名对应的 scope 权限;如果没有 `@nd` scope,需要先改包名,例如改成你实际拥有的 scope,或改成非 scope 包名。
|
|
76
|
+
2. 如果要让中国大陆用户安装体验更稳定,建议仍以 npmjs 作为唯一发布源,再让用户通过 `npmmirror` 安装;不要把 GitHub Packages 当作主发布仓库,因为安装门槛和网络可达性都更差。
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CliCommandError } from '../lib/errors.js';
|
|
2
|
+
import { getRequiredString, pickDefined } from '../lib/payload.js';
|
|
3
|
+
const terminalProductionStatuses = new Set(['succeeded', 'failed', 'interrupted', 'cancelled']);
|
|
4
|
+
async function waitForProduction(projectId, handler) {
|
|
5
|
+
const { client, flags, runtime } = handler;
|
|
6
|
+
const startedAt = runtime.now();
|
|
7
|
+
while (runtime.now() - startedAt <= flags.timeoutMs) {
|
|
8
|
+
const status = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/production`, {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
});
|
|
11
|
+
const statusValue = String(status.data?.status || '');
|
|
12
|
+
if (terminalProductionStatuses.has(statusValue)) {
|
|
13
|
+
if (statusValue !== 'succeeded') {
|
|
14
|
+
throw new CliCommandError(String(status.data?.error || 'Production failed'), 'PRODUCTION_FAILED', status.data, status.requestId);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
data: status.data,
|
|
18
|
+
requestId: status.requestId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
await runtime.sleep(flags.pollIntervalMs);
|
|
22
|
+
}
|
|
23
|
+
throw new CliCommandError('Production wait timed out', 'TIMEOUT', { projectId });
|
|
24
|
+
}
|
|
25
|
+
export const productionCommands = {
|
|
26
|
+
schema: async ({ client, payload }) => {
|
|
27
|
+
const result = await client.request('/api/branch-video/production/aihub/schema', {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
query: pickDefined(payload, ['mode']),
|
|
30
|
+
});
|
|
31
|
+
return { data: result.data, requestId: result.requestId };
|
|
32
|
+
},
|
|
33
|
+
start: async (context) => {
|
|
34
|
+
const result = await context.client.request('/api/branch-video/production/aihub/start', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: context.payload,
|
|
37
|
+
});
|
|
38
|
+
if (!context.flags.wait) {
|
|
39
|
+
return { data: result.data, requestId: result.requestId };
|
|
40
|
+
}
|
|
41
|
+
const projectId = getRequiredString(result.data, 'projectId');
|
|
42
|
+
const finalStatus = await waitForProduction(projectId, context);
|
|
43
|
+
return {
|
|
44
|
+
data: {
|
|
45
|
+
start: result.data,
|
|
46
|
+
status: finalStatus.data,
|
|
47
|
+
},
|
|
48
|
+
requestId: finalStatus.requestId || result.requestId,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
status: async ({ client, payload }) => {
|
|
52
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
53
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/production`, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
});
|
|
56
|
+
return { data: result.data, requestId: result.requestId };
|
|
57
|
+
},
|
|
58
|
+
retry: async (context) => {
|
|
59
|
+
const projectId = getRequiredString(context.payload, 'projectId');
|
|
60
|
+
const result = await context.client.request(`/api/branch-video/${encodeURIComponent(projectId)}/production/retry`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
});
|
|
63
|
+
if (!context.flags.wait) {
|
|
64
|
+
return { data: result.data, requestId: result.requestId };
|
|
65
|
+
}
|
|
66
|
+
const finalStatus = await waitForProduction(projectId, context);
|
|
67
|
+
return {
|
|
68
|
+
data: {
|
|
69
|
+
retry: result.data,
|
|
70
|
+
status: finalStatus.data,
|
|
71
|
+
},
|
|
72
|
+
requestId: finalStatus.requestId || result.requestId,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getRequiredString, omitKeys, pickDefined } from '../lib/payload.js';
|
|
2
|
+
export const projectCommands = {
|
|
3
|
+
list: async ({ client, payload }) => {
|
|
4
|
+
const result = await client.request('/api/branch-video', {
|
|
5
|
+
method: 'GET',
|
|
6
|
+
query: pickDefined(payload, ['filter', 'page', 'limit', 'sort', 'order']),
|
|
7
|
+
});
|
|
8
|
+
return { data: result.data, requestId: result.requestId };
|
|
9
|
+
},
|
|
10
|
+
get: async ({ client, payload }) => {
|
|
11
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
12
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/project`, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
});
|
|
15
|
+
return { data: result.data, requestId: result.requestId };
|
|
16
|
+
},
|
|
17
|
+
create: async ({ client, payload }) => {
|
|
18
|
+
const result = await client.request('/api/branch-video', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
body: payload,
|
|
21
|
+
});
|
|
22
|
+
return { data: result.data, requestId: result.requestId };
|
|
23
|
+
},
|
|
24
|
+
update: async ({ client, payload }) => {
|
|
25
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
26
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}`, {
|
|
27
|
+
method: 'PUT',
|
|
28
|
+
body: omitKeys(payload, ['projectId']),
|
|
29
|
+
});
|
|
30
|
+
return { data: result.data, requestId: result.requestId };
|
|
31
|
+
},
|
|
32
|
+
delete: async ({ client, payload }) => {
|
|
33
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
34
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}`, {
|
|
35
|
+
method: 'DELETE',
|
|
36
|
+
});
|
|
37
|
+
return { data: result.data, requestId: result.requestId };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getRequiredInt, getRequiredString, omitKeys } from '../lib/payload.js';
|
|
2
|
+
export const versionCommands = {
|
|
3
|
+
list: async ({ client, payload }) => {
|
|
4
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
5
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/versions`, {
|
|
6
|
+
method: 'GET',
|
|
7
|
+
});
|
|
8
|
+
return { data: result.data, requestId: result.requestId };
|
|
9
|
+
},
|
|
10
|
+
get: async ({ client, payload }) => {
|
|
11
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
12
|
+
const version = getRequiredInt(payload, 'version');
|
|
13
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/versions/${version}`, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
});
|
|
16
|
+
return { data: result.data, requestId: result.requestId };
|
|
17
|
+
},
|
|
18
|
+
publish: async ({ client, payload }) => {
|
|
19
|
+
const projectId = getRequiredString(payload, 'projectId');
|
|
20
|
+
const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}/publish`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
body: omitKeys(payload, ['projectId']),
|
|
23
|
+
});
|
|
24
|
+
return { data: result.data, requestId: result.requestId };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CliCommandError } from '../lib/errors.js';
|
|
2
|
+
import { getRequiredString } from '../lib/payload.js';
|
|
3
|
+
const terminalWorkflowStatuses = new Set(['succeeded', 'failed']);
|
|
4
|
+
export const workflowCommands = {
|
|
5
|
+
run: async ({ client, payload, flags, runtime }) => {
|
|
6
|
+
const run = await client.request('/api/agent/aihub/workflows/run', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
body: payload,
|
|
9
|
+
});
|
|
10
|
+
if (!flags.wait) {
|
|
11
|
+
return { data: run.data, requestId: run.requestId };
|
|
12
|
+
}
|
|
13
|
+
const runId = getRequiredString(run.data, 'runId');
|
|
14
|
+
const startedAt = runtime.now();
|
|
15
|
+
while (runtime.now() - startedAt <= flags.timeoutMs) {
|
|
16
|
+
const status = await client.request(`/api/agent/aihub/workflows/runs/${encodeURIComponent(runId)}`, {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
});
|
|
19
|
+
const statusValue = String(status.data?.status || '');
|
|
20
|
+
if (terminalWorkflowStatuses.has(statusValue)) {
|
|
21
|
+
if (statusValue !== 'succeeded') {
|
|
22
|
+
throw new CliCommandError(String(status.data?.error?.message || status.data?.error || 'Workflow failed'), 'RUN_FAILED', { run: run.data, status: status.data }, status.requestId);
|
|
23
|
+
}
|
|
24
|
+
const outputs = await client.request(`/api/agent/aihub/workflows/runs/${encodeURIComponent(runId)}/outputs`, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
data: {
|
|
29
|
+
run: run.data,
|
|
30
|
+
status: status.data,
|
|
31
|
+
outputs: outputs.data,
|
|
32
|
+
},
|
|
33
|
+
requestId: outputs.requestId || status.requestId || run.requestId,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
await runtime.sleep(flags.pollIntervalMs);
|
|
37
|
+
}
|
|
38
|
+
throw new CliCommandError('Workflow wait timed out', 'TIMEOUT', { runId });
|
|
39
|
+
},
|
|
40
|
+
status: async ({ client, payload }) => {
|
|
41
|
+
const runId = getRequiredString(payload, 'runId');
|
|
42
|
+
const result = await client.request(`/api/agent/aihub/workflows/runs/${encodeURIComponent(runId)}`, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
});
|
|
45
|
+
return { data: result.data, requestId: result.requestId };
|
|
46
|
+
},
|
|
47
|
+
outputs: async ({ client, payload }) => {
|
|
48
|
+
const runId = getRequiredString(payload, 'runId');
|
|
49
|
+
const result = await client.request(`/api/agent/aihub/workflows/runs/${encodeURIComponent(runId)}/outputs`, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
});
|
|
52
|
+
return { data: result.data, requestId: result.requestId };
|
|
53
|
+
},
|
|
54
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { productionCommands } from './commands/production.js';
|
|
2
|
+
import { projectCommands } from './commands/project.js';
|
|
3
|
+
import { versionCommands } from './commands/version.js';
|
|
4
|
+
import { workflowCommands } from './commands/workflow.js';
|
|
5
|
+
import { CliCommandError } from './lib/errors.js';
|
|
6
|
+
import { getCliHelpText, parseCliArgs, resolveCliConfig } from './lib/flags.js';
|
|
7
|
+
import { CliHttpClient } from './lib/http.js';
|
|
8
|
+
import { ensureObject } from './lib/payload.js';
|
|
9
|
+
import { createProcessRuntime } from './lib/runtime.js';
|
|
10
|
+
const commandRegistry = {
|
|
11
|
+
workflow: workflowCommands,
|
|
12
|
+
project: projectCommands,
|
|
13
|
+
version: versionCommands,
|
|
14
|
+
production: productionCommands,
|
|
15
|
+
};
|
|
16
|
+
function normalizeError(error) {
|
|
17
|
+
if (error instanceof CliCommandError) {
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return new CliCommandError(error.message || 'Unexpected error');
|
|
22
|
+
}
|
|
23
|
+
return new CliCommandError(String(error || 'Unexpected error'));
|
|
24
|
+
}
|
|
25
|
+
function writeJson(runtime, value) {
|
|
26
|
+
runtime.writeStdout(`${JSON.stringify(value, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
function writeError(runtime, command, raw, error) {
|
|
29
|
+
if (raw) {
|
|
30
|
+
writeJson(runtime, error.data ?? {
|
|
31
|
+
error: error.message,
|
|
32
|
+
code: error.code,
|
|
33
|
+
requestId: error.requestId || null,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
writeJson(runtime, {
|
|
38
|
+
ok: false,
|
|
39
|
+
data: null,
|
|
40
|
+
error: error.message,
|
|
41
|
+
code: error.code,
|
|
42
|
+
requestId: error.requestId || null,
|
|
43
|
+
meta: { command },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function main(argv, runtime = createProcessRuntime()) {
|
|
47
|
+
let commandName = '';
|
|
48
|
+
let rawOutput = false;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = parseCliArgs(argv);
|
|
51
|
+
rawOutput = parsed.flags.raw;
|
|
52
|
+
commandName = [parsed.group, parsed.action].filter(Boolean).join('.');
|
|
53
|
+
if (parsed.flags.help || !parsed.group || !parsed.action) {
|
|
54
|
+
runtime.writeStdout(`${getCliHelpText()}\n`);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
const group = commandRegistry[parsed.group];
|
|
58
|
+
const command = group?.[parsed.action];
|
|
59
|
+
if (!group || !command) {
|
|
60
|
+
throw new CliCommandError(`Unknown command: ${commandName || argv.join(' ')}`, 'VALIDATION_ERROR');
|
|
61
|
+
}
|
|
62
|
+
const stdinText = await runtime.readStdinText();
|
|
63
|
+
let stdinPayload = {};
|
|
64
|
+
if (stdinText.trim()) {
|
|
65
|
+
try {
|
|
66
|
+
stdinPayload = ensureObject(JSON.parse(stdinText));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new CliCommandError(`Invalid stdin JSON: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const { baseUrl, token, commandPayload } = await resolveCliConfig(stdinPayload, parsed.flags, runtime);
|
|
73
|
+
const client = new CliHttpClient(baseUrl, token, runtime.fetchImpl);
|
|
74
|
+
const result = await command({
|
|
75
|
+
payload: commandPayload,
|
|
76
|
+
flags: parsed.flags,
|
|
77
|
+
client,
|
|
78
|
+
runtime,
|
|
79
|
+
});
|
|
80
|
+
if (parsed.flags.raw) {
|
|
81
|
+
writeJson(runtime, result.data);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
writeJson(runtime, {
|
|
85
|
+
ok: true,
|
|
86
|
+
data: result.data,
|
|
87
|
+
error: null,
|
|
88
|
+
code: null,
|
|
89
|
+
requestId: result.requestId || null,
|
|
90
|
+
meta: {
|
|
91
|
+
command: commandName,
|
|
92
|
+
wait: parsed.flags.wait,
|
|
93
|
+
...(result.meta || {}),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
writeError(runtime, commandName, rawOutput, normalizeError(error));
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class CliCommandError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
data;
|
|
4
|
+
requestId;
|
|
5
|
+
status;
|
|
6
|
+
constructor(message, code = 'CLI_ERROR', data, requestId, status) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CliCommandError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.data = data;
|
|
11
|
+
this.requestId = requestId;
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type JsonObject } from './payload.js';
|
|
2
|
+
import type { CliRuntime } from './runtime.js';
|
|
3
|
+
export type CliFlags = {
|
|
4
|
+
raw: boolean;
|
|
5
|
+
wait: boolean;
|
|
6
|
+
help: boolean;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
pollIntervalMs: number;
|
|
9
|
+
baseUrlArg: string;
|
|
10
|
+
tokenArg: string;
|
|
11
|
+
tokenFile: string;
|
|
12
|
+
};
|
|
13
|
+
export type ParsedCliArgs = {
|
|
14
|
+
group: string;
|
|
15
|
+
action: string;
|
|
16
|
+
flags: CliFlags;
|
|
17
|
+
};
|
|
18
|
+
export declare function resolveCliConfig(payload: JsonObject, flags: CliFlags, runtime: CliRuntime): Promise<{
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
token: string;
|
|
21
|
+
commandPayload: JsonObject;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
|
|
24
|
+
export declare function getCliHelpText(): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CliCommandError } from './errors.js';
|
|
2
|
+
import { getOptionalString, omitKeys } from './payload.js';
|
|
3
|
+
export async function resolveCliConfig(payload, flags, runtime) {
|
|
4
|
+
const envBaseUrl = String(runtime.env.BRANCH_VIDEO_AGENT_BASE_URL || '').trim();
|
|
5
|
+
const envToken = String(runtime.env.AIHUB_AGENT_TOKEN || '').trim();
|
|
6
|
+
const stdinBaseUrl = getOptionalString(payload, 'baseUrl');
|
|
7
|
+
const stdinToken = getOptionalString(payload, 'token');
|
|
8
|
+
const fileToken = flags.tokenFile ? String(await runtime.readTextFile(flags.tokenFile)).trim() : '';
|
|
9
|
+
const baseUrl = envBaseUrl || stdinBaseUrl || flags.baseUrlArg;
|
|
10
|
+
const token = envToken || stdinToken || fileToken || flags.tokenArg;
|
|
11
|
+
if (!baseUrl) {
|
|
12
|
+
throw new CliCommandError('Missing base URL', 'VALIDATION_ERROR');
|
|
13
|
+
}
|
|
14
|
+
if (!token) {
|
|
15
|
+
throw new CliCommandError('Missing agent token', 'VALIDATION_ERROR');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
baseUrl,
|
|
19
|
+
token,
|
|
20
|
+
commandPayload: omitKeys(payload, ['baseUrl', 'token']),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function readValue(args, index, flag) {
|
|
24
|
+
const value = args[index + 1];
|
|
25
|
+
if (!value || value.startsWith('--')) {
|
|
26
|
+
throw new CliCommandError(`Missing value for ${flag}`, 'VALIDATION_ERROR');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function parseCliArgs(argv) {
|
|
31
|
+
const positionals = [];
|
|
32
|
+
const flags = {
|
|
33
|
+
raw: false,
|
|
34
|
+
wait: false,
|
|
35
|
+
help: false,
|
|
36
|
+
timeoutMs: 300000,
|
|
37
|
+
pollIntervalMs: 1000,
|
|
38
|
+
baseUrlArg: '',
|
|
39
|
+
tokenArg: '',
|
|
40
|
+
tokenFile: '',
|
|
41
|
+
};
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
const arg = argv[index];
|
|
44
|
+
if (!arg.startsWith('--')) {
|
|
45
|
+
positionals.push(arg);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
switch (arg) {
|
|
49
|
+
case '--help':
|
|
50
|
+
case '-h':
|
|
51
|
+
flags.help = true;
|
|
52
|
+
break;
|
|
53
|
+
case '--raw':
|
|
54
|
+
flags.raw = true;
|
|
55
|
+
break;
|
|
56
|
+
case '--wait':
|
|
57
|
+
flags.wait = true;
|
|
58
|
+
break;
|
|
59
|
+
case '--timeout':
|
|
60
|
+
flags.timeoutMs = Number(readValue(argv, index, arg));
|
|
61
|
+
index += 1;
|
|
62
|
+
break;
|
|
63
|
+
case '--poll-interval':
|
|
64
|
+
flags.pollIntervalMs = Number(readValue(argv, index, arg));
|
|
65
|
+
index += 1;
|
|
66
|
+
break;
|
|
67
|
+
case '--base-url':
|
|
68
|
+
flags.baseUrlArg = readValue(argv, index, arg).trim();
|
|
69
|
+
index += 1;
|
|
70
|
+
break;
|
|
71
|
+
case '--token':
|
|
72
|
+
flags.tokenArg = readValue(argv, index, arg).trim();
|
|
73
|
+
index += 1;
|
|
74
|
+
break;
|
|
75
|
+
case '--token-file':
|
|
76
|
+
flags.tokenFile = readValue(argv, index, arg).trim();
|
|
77
|
+
index += 1;
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
throw new CliCommandError(`Unknown flag: ${arg}`, 'VALIDATION_ERROR');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
group: positionals[0] || '',
|
|
85
|
+
action: positionals[1] || '',
|
|
86
|
+
flags,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function getCliHelpText() {
|
|
90
|
+
return [
|
|
91
|
+
'Usage: branch-video-agent <group> <action> [--wait] [--raw] [--timeout <ms>] [--base-url <url>] [--token <token>] [--token-file <path>]',
|
|
92
|
+
'',
|
|
93
|
+
'Groups:',
|
|
94
|
+
' workflow run | status | outputs',
|
|
95
|
+
' project list | get | create | update | delete',
|
|
96
|
+
' version list | get | publish',
|
|
97
|
+
' production schema | start | status | retry',
|
|
98
|
+
'',
|
|
99
|
+
'Config precedence:',
|
|
100
|
+
' AIHUB_AGENT_TOKEN / BRANCH_VIDEO_AGENT_BASE_URL > stdin JSON or --token-file > command flags',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type RequestOptions = {
|
|
2
|
+
method: string;
|
|
3
|
+
body?: unknown;
|
|
4
|
+
query?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
type RequestResult<T> = {
|
|
7
|
+
data: T;
|
|
8
|
+
status: number;
|
|
9
|
+
requestId: string | null;
|
|
10
|
+
};
|
|
11
|
+
export declare class CliHttpClient {
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly token;
|
|
14
|
+
private readonly fetchImpl;
|
|
15
|
+
constructor(baseUrl: string, token: string, fetchImpl: typeof fetch);
|
|
16
|
+
request<T>(path: string, options: RequestOptions): Promise<RequestResult<T>>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CliCommandError } from './errors.js';
|
|
2
|
+
function normalizeBaseUrl(baseUrl) {
|
|
3
|
+
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
4
|
+
}
|
|
5
|
+
function buildUrl(baseUrl, path, query) {
|
|
6
|
+
const url = new URL(path.replace(/^\//, ''), normalizeBaseUrl(baseUrl));
|
|
7
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
8
|
+
if (value === undefined || value === null || value === '')
|
|
9
|
+
continue;
|
|
10
|
+
url.searchParams.set(key, String(value));
|
|
11
|
+
}
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
function extractRequestId(response, parsed) {
|
|
15
|
+
return response.headers.get('x-request-id')
|
|
16
|
+
|| response.headers.get('x-correlation-id')
|
|
17
|
+
|| (typeof parsed?.requestId === 'string' ? parsed.requestId : null);
|
|
18
|
+
}
|
|
19
|
+
export class CliHttpClient {
|
|
20
|
+
baseUrl;
|
|
21
|
+
token;
|
|
22
|
+
fetchImpl;
|
|
23
|
+
constructor(baseUrl, token, fetchImpl) {
|
|
24
|
+
this.baseUrl = baseUrl;
|
|
25
|
+
this.token = token;
|
|
26
|
+
this.fetchImpl = fetchImpl;
|
|
27
|
+
}
|
|
28
|
+
async request(path, options) {
|
|
29
|
+
const headers = {
|
|
30
|
+
Authorization: `Bearer ${this.token}`,
|
|
31
|
+
};
|
|
32
|
+
if (options.body !== undefined) {
|
|
33
|
+
headers['content-type'] = 'application/json';
|
|
34
|
+
}
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await this.fetchImpl(buildUrl(this.baseUrl, path, options.query), {
|
|
38
|
+
method: options.method,
|
|
39
|
+
headers,
|
|
40
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new CliCommandError(error?.message || 'Network request failed', 'NETWORK_ERROR');
|
|
45
|
+
}
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
const parsed = text
|
|
48
|
+
? (() => {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
56
|
+
: null;
|
|
57
|
+
const requestId = extractRequestId(response, parsed);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const errorMessage = typeof parsed === 'object' && parsed && typeof parsed.error === 'string'
|
|
60
|
+
? String(parsed.error)
|
|
61
|
+
: response.statusText || 'Request failed';
|
|
62
|
+
const errorCode = typeof parsed === 'object' && parsed && typeof parsed.code === 'string'
|
|
63
|
+
? String(parsed.code)
|
|
64
|
+
: `HTTP_${response.status}`;
|
|
65
|
+
throw new CliCommandError(errorMessage, errorCode, parsed, requestId, response.status);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
data: parsed,
|
|
69
|
+
status: response.status,
|
|
70
|
+
requestId,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type JsonObject = Record<string, unknown>;
|
|
2
|
+
export declare function ensureObject(value: unknown, fieldName?: string): JsonObject;
|
|
3
|
+
export declare function getRequiredString(payload: JsonObject, key: string): string;
|
|
4
|
+
export declare function getOptionalString(payload: JsonObject, key: string): string;
|
|
5
|
+
export declare function getRequiredInt(payload: JsonObject, key: string): number;
|
|
6
|
+
export declare function omitKeys(payload: JsonObject, keys: string[]): JsonObject;
|
|
7
|
+
export declare function pickDefined(payload: JsonObject, keys: string[]): JsonObject;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CliCommandError } from './errors.js';
|
|
2
|
+
export function ensureObject(value, fieldName = 'stdin payload') {
|
|
3
|
+
if (!value)
|
|
4
|
+
return {};
|
|
5
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
6
|
+
throw new CliCommandError(`${fieldName} must be a JSON object`, 'VALIDATION_ERROR');
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function getRequiredString(payload, key) {
|
|
11
|
+
const value = payload[key];
|
|
12
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
13
|
+
throw new CliCommandError(`Missing ${key}`, 'VALIDATION_ERROR');
|
|
14
|
+
}
|
|
15
|
+
return value.trim();
|
|
16
|
+
}
|
|
17
|
+
export function getOptionalString(payload, key) {
|
|
18
|
+
const value = payload[key];
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return '';
|
|
21
|
+
return value.trim();
|
|
22
|
+
}
|
|
23
|
+
export function getRequiredInt(payload, key) {
|
|
24
|
+
const value = payload[key];
|
|
25
|
+
const normalized = typeof value === 'string' ? value.trim() : value;
|
|
26
|
+
const parsed = typeof normalized === 'number' ? normalized : Number(normalized);
|
|
27
|
+
if (!Number.isInteger(parsed)) {
|
|
28
|
+
throw new CliCommandError(`Invalid ${key}`, 'VALIDATION_ERROR');
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
export function omitKeys(payload, keys) {
|
|
33
|
+
const omitted = new Set(keys);
|
|
34
|
+
return Object.entries(payload).reduce((acc, [key, value]) => {
|
|
35
|
+
if (!omitted.has(key)) {
|
|
36
|
+
acc[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return acc;
|
|
39
|
+
}, {});
|
|
40
|
+
}
|
|
41
|
+
export function pickDefined(payload, keys) {
|
|
42
|
+
return keys.reduce((acc, key) => {
|
|
43
|
+
const value = payload[key];
|
|
44
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
45
|
+
acc[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return acc;
|
|
48
|
+
}, {});
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CliRuntime {
|
|
2
|
+
env: Record<string, string | undefined>;
|
|
3
|
+
fetchImpl: typeof fetch;
|
|
4
|
+
readStdinText: () => Promise<string>;
|
|
5
|
+
readTextFile: (filePath: string) => Promise<string>;
|
|
6
|
+
writeStdout: (text: string) => void;
|
|
7
|
+
writeStderr: (text: string) => void;
|
|
8
|
+
sleep: (ms: number) => Promise<void>;
|
|
9
|
+
now: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createProcessRuntime(): CliRuntime;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
async function readProcessStdinText() {
|
|
3
|
+
if (process.stdin.isTTY)
|
|
4
|
+
return '';
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of process.stdin) {
|
|
7
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
8
|
+
}
|
|
9
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
10
|
+
}
|
|
11
|
+
export function createProcessRuntime() {
|
|
12
|
+
return {
|
|
13
|
+
env: process.env,
|
|
14
|
+
fetchImpl: fetch,
|
|
15
|
+
readStdinText: readProcessStdinText,
|
|
16
|
+
readTextFile: (filePath) => readFile(filePath, 'utf8'),
|
|
17
|
+
writeStdout: (text) => {
|
|
18
|
+
process.stdout.write(text);
|
|
19
|
+
},
|
|
20
|
+
writeStderr: (text) => {
|
|
21
|
+
process.stderr.write(text);
|
|
22
|
+
},
|
|
23
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
24
|
+
now: () => Date.now(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CliFlags } from './flags.js';
|
|
2
|
+
import type { CliHttpClient } from './http.js';
|
|
3
|
+
import type { JsonObject } from './payload.js';
|
|
4
|
+
import type { CliRuntime } from './runtime.js';
|
|
5
|
+
export type CommandResult = {
|
|
6
|
+
data: unknown;
|
|
7
|
+
requestId?: string | null;
|
|
8
|
+
meta?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export type CommandContext = {
|
|
11
|
+
payload: JsonObject;
|
|
12
|
+
flags: CliFlags;
|
|
13
|
+
client: CliHttpClient;
|
|
14
|
+
runtime: CliRuntime;
|
|
15
|
+
};
|
|
16
|
+
export type CommandHandler = (context: CommandContext) => Promise<CommandResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindscraft/branch-video-agent-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Published CLI for branch-video and AIHub agent APIs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"branch-video-agent": "./dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"branch-video",
|
|
26
|
+
"aihub",
|
|
27
|
+
"cli",
|
|
28
|
+
"interactive-video"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"smoke:help": "npm run build && node dist/bin.js --help"
|
|
34
|
+
}
|
|
35
|
+
}
|