@ngocsangairvds/vsaf 3.2.11 → 3.2.13
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/package.json +1 -1
- package/src/global.js +44 -8
- package/tools/skills/vds-scripts-skill/SKILL.md +1 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +2 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/vidp_tools.py +64 -0
- package/tools/vds-scripts/pyproject.toml +1 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +6 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +5 -0
- package/tools/vds-scripts/vidp_orchestrator/README.md +31 -0
- package/tools/vds-scripts/vidp_orchestrator/pyproject.toml +50 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/__init__.py +26 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/cli.py +246 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/client.py +104 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/config.py +82 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/workflows.json +3 -0
- package/tools/vds-scripts/vidp_orchestrator/src/vds_vidp_orchestrator/workflows.py +130 -0
package/package.json
CHANGED
package/src/global.js
CHANGED
|
@@ -140,10 +140,13 @@ async function setupVdsScriptsMcp() {
|
|
|
140
140
|
ok('Đã sao chép vds-scripts.');
|
|
141
141
|
|
|
142
142
|
info('Đang khởi tạo môi trường Python (uv sync)...');
|
|
143
|
-
|
|
144
|
-
exec(`uv sync --project "${destDir}"`, { silent: true });
|
|
143
|
+
const syncOk = exec(`uv sync --frozen --package vds-cli --project "${destDir}"`, { silent: true })
|
|
144
|
+
|| exec(`uv sync --package vds-cli --project "${destDir}"`, { silent: true });
|
|
145
|
+
if (syncOk) {
|
|
146
|
+
ok('Môi trường Python đã sẵn sàng.');
|
|
147
|
+
} else {
|
|
148
|
+
warn('uv sync thất bại — vds-cli có thể chưa sẵn sàng');
|
|
145
149
|
}
|
|
146
|
-
ok('Môi trường Python đã sẵn sàng.');
|
|
147
150
|
|
|
148
151
|
installVdsCliwrapper(destDir);
|
|
149
152
|
|
|
@@ -160,11 +163,13 @@ async function setupVdsScriptsMcp() {
|
|
|
160
163
|
const jiraToken = await promptSecret(' Nhập JIRA_TOKEN: ');
|
|
161
164
|
const confluenceToken = await promptSecret(' Nhập INTERNAL_CONFLUENCE_TOKEN: ');
|
|
162
165
|
const extConfluenceToken = await promptSecret(' Nhập EXTERNAL_CONFLUENCE_TOKEN: ');
|
|
166
|
+
const vidpUsername = (await promptInput(' Nhập VDS_VIDP_USERNAME: '));
|
|
167
|
+
const vidpPassword = (await promptSecret(' Nhập VDS_VIDP_PASSWORD: '));
|
|
163
168
|
|
|
164
169
|
const confluenceInternalUrl = (await promptInput(' Nhập CONFLUENCE_INTERNAL_URL (ex: http://10.254.136.35:8090): ')) || 'http://10.254.136.35:8090';
|
|
165
|
-
const confluenceExternalUrl = (await promptInput(' Nhập CONFLUENCE_EXTERNAL_URL (ex: https://atlassian.digital.vn): '))
|
|
170
|
+
const confluenceExternalUrl = (await promptInput(' Nhập CONFLUENCE_EXTERNAL_URL (ex: https://atlassian.digital.vn): '));
|
|
166
171
|
|
|
167
|
-
const
|
|
172
|
+
const envLines = [
|
|
168
173
|
`VDS_USERNAME=${vdsUsername}`,
|
|
169
174
|
`VDS_PASSWORD=${vdsPassword}`,
|
|
170
175
|
`BITBUCKET_TOKEN=${bitbucketToken}`,
|
|
@@ -174,13 +179,17 @@ async function setupVdsScriptsMcp() {
|
|
|
174
179
|
'JIRA_BASE_URL=https://jira.viettelmoney.vn/',
|
|
175
180
|
`CONFLUENCE_INTERNAL_URL=${confluenceInternalUrl}`,
|
|
176
181
|
`CONFLUENCE_EXTERNAL_URL=${confluenceExternalUrl}`,
|
|
177
|
-
'BITBUCKET_BASE_URL=http://bitbucket.digital.vn'
|
|
178
|
-
|
|
182
|
+
'BITBUCKET_BASE_URL=http://bitbucket.digital.vn',
|
|
183
|
+
'VDS_VIDP_BASE_URL=http://cloud-idp.digital.vn/api/idp-gw',
|
|
184
|
+
`VDS_VIDP_USERNAME=${vidpUsername}`,
|
|
185
|
+
`VDS_VIDP_PASSWORD=${vidpPassword}`,
|
|
186
|
+
];
|
|
179
187
|
|
|
180
|
-
fs.writeFileSync(envFile,
|
|
188
|
+
fs.writeFileSync(envFile, envLines.join('\n') + '\n');
|
|
181
189
|
ok('Đã lưu cấu hình an toàn vào ~/.vds/.env');
|
|
182
190
|
} else {
|
|
183
191
|
ok('Đã tìm thấy file cấu hình ~/.vds/.env');
|
|
192
|
+
await ensureVidpEnv(envFile);
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
exec('claude mcp remove vds-orchestrator', { silent: true });
|
|
@@ -193,6 +202,33 @@ async function setupVdsScriptsMcp() {
|
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
async function ensureVidpEnv(envFile) {
|
|
206
|
+
const current = fs.readFileSync(envFile, 'utf8');
|
|
207
|
+
if (/^\s*(VDS_)?VIDP_BASE_URL\s*=/m.test(current)) return;
|
|
208
|
+
|
|
209
|
+
const readKey = (key) => {
|
|
210
|
+
const match = current.match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, 'm'));
|
|
211
|
+
return match ? match[1].trim() : '';
|
|
212
|
+
};
|
|
213
|
+
const vdsUsername = readKey('VDS_USERNAME');
|
|
214
|
+
const vdsPassword = readKey('VDS_PASSWORD');
|
|
215
|
+
|
|
216
|
+
info('VIDP third-party gateway chưa được cấu hình — sẽ dùng VDS credentials (Enter để chấp nhận, nhập giá trị khác để override):');
|
|
217
|
+
const vidpUsername = (await promptInput(` Nhập VDS_VIDP_USERNAME (Enter để dùng "${vdsUsername}"): `)) || vdsUsername;
|
|
218
|
+
const vidpPassword = (await promptSecret(' Nhập VDS_VIDP_PASSWORD (Enter để dùng VDS_PASSWORD): ')) || vdsPassword;
|
|
219
|
+
|
|
220
|
+
const suffix = [
|
|
221
|
+
'',
|
|
222
|
+
'VDS_VIDP_BASE_URL=http://cloud-idp.digital.vn/api/idp-gw',
|
|
223
|
+
`VDS_VIDP_USERNAME=${vidpUsername}`,
|
|
224
|
+
`VDS_VIDP_PASSWORD=${vidpPassword}`,
|
|
225
|
+
'',
|
|
226
|
+
].join('\n');
|
|
227
|
+
const sep = current.endsWith('\n') ? '' : '\n';
|
|
228
|
+
fs.appendFileSync(envFile, sep + suffix);
|
|
229
|
+
ok('Đã thêm cấu hình VIDP vào ~/.vds/.env');
|
|
230
|
+
}
|
|
231
|
+
|
|
196
232
|
function promptInput(question) {
|
|
197
233
|
return new Promise((resolve) => {
|
|
198
234
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -17,6 +17,7 @@ from .tools import (
|
|
|
17
17
|
register_confluence_tools,
|
|
18
18
|
register_git_tools,
|
|
19
19
|
register_jira_tools,
|
|
20
|
+
register_vidp_tools,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
# Initialize FastMCP server
|
|
@@ -51,6 +52,7 @@ register_jira_tools(mcp, execute_vds_cli)
|
|
|
51
52
|
register_confluence_tools(mcp, execute_vds_cli)
|
|
52
53
|
register_bitbucket_tools(mcp, execute_vds_cli)
|
|
53
54
|
register_git_tools(mcp, execute_vds_cli)
|
|
55
|
+
register_vidp_tools(mcp, execute_vds_cli)
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
try:
|
|
@@ -4,10 +4,12 @@ from .bitbucket_tools import register_bitbucket_tools
|
|
|
4
4
|
from .confluence_tools import register_confluence_tools
|
|
5
5
|
from .git_tools import register_git_tools
|
|
6
6
|
from .jira_tools import register_jira_tools
|
|
7
|
+
from .vidp_tools import register_vidp_tools
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"register_bitbucket_tools",
|
|
10
11
|
"register_confluence_tools",
|
|
11
12
|
"register_git_tools",
|
|
12
13
|
"register_jira_tools",
|
|
14
|
+
"register_vidp_tools",
|
|
13
15
|
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""VIDP tools for MCP server.
|
|
2
|
+
|
|
3
|
+
Thin wrappers around ``vds-cli vidp ...`` so the VIDP gateway is reachable
|
|
4
|
+
from any MCP client that talks to the ``vds-orchestrator`` server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_vidp_tools(mcp: FastMCP, execute_cli: Callable[..., Any]) -> None:
|
|
18
|
+
"""Register VIDP tools with the MCP server."""
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def vidp_trigger(workflow: str, payload: Any | None = None) -> str:
|
|
22
|
+
"""Trigger a VIDP third-party action by workflow UUID or alias.
|
|
23
|
+
|
|
24
|
+
Maps to ``POST /third-party/v2/start/{workflow_id}``.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
workflow: VIDP workflow UUID (e.g. ``4afa9a1d-d36c-441a-b2fd-ba9612a854c1``)
|
|
28
|
+
OR a friendly alias defined in ``~/.vds/vidp-workflows.json``
|
|
29
|
+
(e.g. ``build-jenkins-by-job-name``). Aliases are case-insensitive.
|
|
30
|
+
payload: Optional JSON payload. Any JSON-serialisable object;
|
|
31
|
+
does not need to start with ``tenantToken``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
JSON string with endpoint, alias used (if any), workflowId, request
|
|
35
|
+
payload, and full response.
|
|
36
|
+
"""
|
|
37
|
+
args: list[str] = ["vidp", "trigger", workflow]
|
|
38
|
+
if payload is not None:
|
|
39
|
+
args.extend(["--payload", json.dumps(payload)])
|
|
40
|
+
result = await execute_cli(*args)
|
|
41
|
+
if result["returncode"] != 0:
|
|
42
|
+
return f"Error: {result['stderr'] or result['stdout']}"
|
|
43
|
+
return result["stdout"]
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
async def vidp_status(job_id: str, raw: bool = False) -> str:
|
|
47
|
+
"""Get full workflow/stage detail for a VIDP job.
|
|
48
|
+
|
|
49
|
+
Maps to ``GET /third-party/v2/status/{job_id}?full=true``.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
job_id: VIDP job id.
|
|
53
|
+
raw: If true, return only the raw VIDP response.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
JSON string with normalized job state and the raw VIDP response.
|
|
57
|
+
"""
|
|
58
|
+
args: list[str] = ["vidp", "status", job_id]
|
|
59
|
+
if raw:
|
|
60
|
+
args.append("--raw")
|
|
61
|
+
result = await execute_cli(*args)
|
|
62
|
+
if result["returncode"] != 0:
|
|
63
|
+
return f"Error: {result['stderr'] or result['stdout']}"
|
|
64
|
+
return result["stdout"]
|
|
@@ -51,6 +51,7 @@ members = [
|
|
|
51
51
|
# "telegram_bridge", # disabled 2026-04-30 — aiogram 3.27 pydantic<2.13 vs memory-orchestrator pydantic>=2.13.3; restore when aiogram >=3.28.0 ships PR #1795
|
|
52
52
|
"vds_sync_orchestrator",
|
|
53
53
|
"text_utils_orchestrator",
|
|
54
|
+
"vidp_orchestrator",
|
|
54
55
|
]
|
|
55
56
|
|
|
56
57
|
[tool.pytest.ini_options]
|
|
@@ -451,6 +451,12 @@ def public_interface(ctx: typer.Context) -> None:
|
|
|
451
451
|
_run_service("public_interface", list(ctx.args))
|
|
452
452
|
|
|
453
453
|
|
|
454
|
+
@app.command(context_settings=PASS_THRU_CONTEXT)
|
|
455
|
+
def vidp(ctx: typer.Context) -> None:
|
|
456
|
+
"""VIDP third-party gateway operations (trigger workflows, poll job status)."""
|
|
457
|
+
_run_service("vidp", list(ctx.args))
|
|
458
|
+
|
|
459
|
+
|
|
454
460
|
@app.command(context_settings=PASS_THRU_CONTEXT)
|
|
455
461
|
def scheduler(ctx: typer.Context) -> None:
|
|
456
462
|
"""Ecosystem scheduler operations (schedule/event/chain/workflow); requires VDS_SCHEDULER_ENABLED=true."""
|
|
@@ -175,6 +175,11 @@ ORCHESTRATORS = {
|
|
|
175
175
|
"path": "public_interface_boundary_orchestrator",
|
|
176
176
|
"entry": "vds-public-interface",
|
|
177
177
|
},
|
|
178
|
+
"vidp": {
|
|
179
|
+
"module": "vds_vidp_orchestrator.cli",
|
|
180
|
+
"path": "vidp_orchestrator",
|
|
181
|
+
"entry": "vds-vidp",
|
|
182
|
+
},
|
|
178
183
|
"scheduler": {
|
|
179
184
|
"module": "vds_scheduler_orchestrator.cli",
|
|
180
185
|
"path": "scheduler_orchestrator",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# vds-vidp-orchestrator
|
|
2
|
+
|
|
3
|
+
VIDP third-party gateway orchestrator for the VDS scripts ecosystem.
|
|
4
|
+
|
|
5
|
+
Wraps two VIDP endpoints:
|
|
6
|
+
|
|
7
|
+
- `POST /third-party/v2/start/{workflowId}` — trigger a workflow action
|
|
8
|
+
- `GET /third-party/v2/status/{jobId}?full=true` — fetch full job/stage detail
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# CLI
|
|
14
|
+
vds-cli vidp trigger <workflowId> --payload '{"key":"value"}'
|
|
15
|
+
vds-cli vidp status <jobId>
|
|
16
|
+
|
|
17
|
+
# Direct
|
|
18
|
+
uv run vds-vidp trigger <workflowId> --payload-file ./payload.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Environment
|
|
22
|
+
|
|
23
|
+
Loaded from the shared VDS env file (`~/.vds/.env`). Settings use prefix
|
|
24
|
+
`VDS_VIDP_` but legacy `VIDP_*` names are accepted as fallbacks.
|
|
25
|
+
|
|
26
|
+
| Variable | Required | Notes |
|
|
27
|
+
| --------------------------------- | -------- | -------------------------------------- |
|
|
28
|
+
| `VDS_VIDP_BASE_URL` / `VIDP_BASE_URL` | yes | Base URL of the VIDP gateway |
|
|
29
|
+
| `VDS_VIDP_USERNAME` / `VIDP_USERNAME` | yes | Basic-auth username |
|
|
30
|
+
| `VDS_VIDP_PASSWORD` / `VIDP_PASSWORD` | yes | Basic-auth password |
|
|
31
|
+
| `VDS_VIDP_TIMEOUT_MS` | no | Request timeout in ms (default `30000`) |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vds-vidp-orchestrator"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "VDS VIDP Orchestrator — trigger third-party workflows and poll job status via VIDP gateway"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "VDS Team" }]
|
|
9
|
+
keywords = ["vidp", "idp", "third-party", "vds", "orchestrator"]
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.28.1",
|
|
13
|
+
"typer>=0.24.1",
|
|
14
|
+
"pydantic>=2.12.5",
|
|
15
|
+
"pydantic-settings>=2.12.0",
|
|
16
|
+
"structlog>=25.5.0",
|
|
17
|
+
"rich>=14.3.3",
|
|
18
|
+
"vds-cli-common",
|
|
19
|
+
"vds-platform-core",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
vds-vidp = "vds_vidp_orchestrator.cli:app"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8.0.0",
|
|
28
|
+
"pytest-httpx>=0.35.0",
|
|
29
|
+
"pytest-asyncio>=0.24.0",
|
|
30
|
+
"ruff>=0.11.12",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling>=1.20.0"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/vds_vidp_orchestrator"]
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
package = true
|
|
42
|
+
|
|
43
|
+
[tool.uv.sources]
|
|
44
|
+
vds_vidp_orchestrator = { workspace = true }
|
|
45
|
+
vds-cli-common = { workspace = true }
|
|
46
|
+
vds-platform-core = { workspace = true }
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py311"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""VDS VIDP Orchestrator package."""
|
|
2
|
+
|
|
3
|
+
from vds_vidp_orchestrator.client import VidpClient
|
|
4
|
+
from vds_vidp_orchestrator.config import VidpSettings, load_settings
|
|
5
|
+
from vds_vidp_orchestrator.workflows import (
|
|
6
|
+
load_aliases,
|
|
7
|
+
load_package_aliases,
|
|
8
|
+
load_user_aliases,
|
|
9
|
+
package_workflows_file,
|
|
10
|
+
resolve_workflow_id,
|
|
11
|
+
save_user_aliases,
|
|
12
|
+
user_workflows_file,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"VidpClient",
|
|
17
|
+
"VidpSettings",
|
|
18
|
+
"load_settings",
|
|
19
|
+
"load_aliases",
|
|
20
|
+
"load_package_aliases",
|
|
21
|
+
"load_user_aliases",
|
|
22
|
+
"package_workflows_file",
|
|
23
|
+
"user_workflows_file",
|
|
24
|
+
"resolve_workflow_id",
|
|
25
|
+
"save_user_aliases",
|
|
26
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""CLI for the VIDP orchestrator.
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
vds-vidp trigger <workflowId>
|
|
5
|
+
vds-vidp trigger <workflowId> --payload '{"foo": "bar"}'
|
|
6
|
+
vds-vidp trigger <workflowId> --payload-file ./payload.json
|
|
7
|
+
vds-vidp status <jobId>
|
|
8
|
+
vds-vidp status <jobId> --raw
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from vds_cli_common.app import make_vds_app
|
|
19
|
+
from vds_cli_common.context import CLIContext
|
|
20
|
+
from vds_cli_common.output import OutputManager
|
|
21
|
+
from vds_platform_core.errors import SettingsError
|
|
22
|
+
from vds_platform_core.logging import configure_structlog
|
|
23
|
+
|
|
24
|
+
from vds_vidp_orchestrator.client import VidpClient
|
|
25
|
+
from vds_vidp_orchestrator.config import load_settings
|
|
26
|
+
from vds_vidp_orchestrator.workflows import (
|
|
27
|
+
load_aliases,
|
|
28
|
+
load_package_aliases,
|
|
29
|
+
load_user_aliases,
|
|
30
|
+
package_workflows_file,
|
|
31
|
+
resolve_workflow_id,
|
|
32
|
+
save_user_aliases,
|
|
33
|
+
user_workflows_file,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
configure_structlog()
|
|
37
|
+
app = make_vds_app(
|
|
38
|
+
name="vds-vidp",
|
|
39
|
+
help="VDS VIDP CLI — trigger third-party workflows and poll job status.",
|
|
40
|
+
settings_factory=load_settings,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_payload(payload: str | None, payload_file: Path | None) -> object | None:
|
|
45
|
+
if payload and payload_file:
|
|
46
|
+
raise typer.BadParameter("Use either --payload or --payload-file, not both.")
|
|
47
|
+
if payload:
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(payload)
|
|
50
|
+
except json.JSONDecodeError as exc:
|
|
51
|
+
raise typer.BadParameter(f"--payload is not valid JSON: {exc}") from exc
|
|
52
|
+
if payload_file:
|
|
53
|
+
try:
|
|
54
|
+
return json.loads(payload_file.read_text(encoding="utf-8"))
|
|
55
|
+
except json.JSONDecodeError as exc:
|
|
56
|
+
raise typer.BadParameter(f"--payload-file is not valid JSON: {exc}") from exc
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def trigger(
|
|
62
|
+
ctx: typer.Context,
|
|
63
|
+
workflow: Annotated[
|
|
64
|
+
str,
|
|
65
|
+
typer.Argument(
|
|
66
|
+
help="Workflow UUID OR alias from ~/.vds/vidp-workflows.json (e.g. 'build-jenkins-by-job-name').",
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
payload: Annotated[
|
|
70
|
+
str | None,
|
|
71
|
+
typer.Option("--payload", "-p", help="Inline JSON payload"),
|
|
72
|
+
] = None,
|
|
73
|
+
payload_file: Annotated[
|
|
74
|
+
Path | None,
|
|
75
|
+
typer.Option(
|
|
76
|
+
"--payload-file",
|
|
77
|
+
"-f",
|
|
78
|
+
exists=True,
|
|
79
|
+
readable=True,
|
|
80
|
+
dir_okay=False,
|
|
81
|
+
help="Path to a JSON file containing the payload",
|
|
82
|
+
),
|
|
83
|
+
] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Trigger a VIDP third-party action by workflow id or alias."""
|
|
86
|
+
out = OutputManager(CLIContext.from_typer_context(ctx))
|
|
87
|
+
try:
|
|
88
|
+
workflow_id, alias_used = resolve_workflow_id(workflow)
|
|
89
|
+
body = _resolve_payload(payload, payload_file)
|
|
90
|
+
settings = load_settings()
|
|
91
|
+
with VidpClient(settings) as client:
|
|
92
|
+
result = client.trigger(workflow_id, body)
|
|
93
|
+
out.output_json(
|
|
94
|
+
{
|
|
95
|
+
"tool": "vidp.trigger",
|
|
96
|
+
"endpoint": f"/third-party/v2/start/{workflow_id}",
|
|
97
|
+
"aliasUsed": alias_used,
|
|
98
|
+
"workflowId": workflow_id,
|
|
99
|
+
"requestPayload": body if body is not None else {},
|
|
100
|
+
"response": result,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
except KeyError as exc:
|
|
104
|
+
out.output_error(str(exc))
|
|
105
|
+
raise typer.Exit(1) from None
|
|
106
|
+
except SettingsError as exc:
|
|
107
|
+
out.output_error(f"Configuration error: {exc}", hint="Check VDS_VIDP_* / VIDP_* env vars.")
|
|
108
|
+
raise typer.Exit(1) from None
|
|
109
|
+
except Exception as exc: # noqa: BLE001 — surface as JSON for AI consumers
|
|
110
|
+
out.output_error(str(exc))
|
|
111
|
+
raise typer.Exit(1) from None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def status(
|
|
116
|
+
ctx: typer.Context,
|
|
117
|
+
job_id: Annotated[
|
|
118
|
+
str,
|
|
119
|
+
typer.Argument(help="Job id (the {jobId} segment of the VIDP status endpoint)"),
|
|
120
|
+
],
|
|
121
|
+
raw: Annotated[
|
|
122
|
+
bool,
|
|
123
|
+
typer.Option("--raw", help="Emit the raw VIDP response only (skip normalization)."),
|
|
124
|
+
] = False,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Fetch full workflow/stage detail for a VIDP job."""
|
|
127
|
+
out = OutputManager(CLIContext.from_typer_context(ctx))
|
|
128
|
+
try:
|
|
129
|
+
settings = load_settings()
|
|
130
|
+
with VidpClient(settings) as client:
|
|
131
|
+
response = client.status(job_id)
|
|
132
|
+
if raw:
|
|
133
|
+
out.output_json(response)
|
|
134
|
+
return
|
|
135
|
+
normalized = VidpClient.normalize_status(response)
|
|
136
|
+
out.output_json(
|
|
137
|
+
{
|
|
138
|
+
"tool": "vidp.status",
|
|
139
|
+
"endpoint": f"/third-party/v2/status/{job_id}?full=true",
|
|
140
|
+
"normalized": normalized,
|
|
141
|
+
"response": response,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
except SettingsError as exc:
|
|
145
|
+
out.output_error(f"Configuration error: {exc}", hint="Check VDS_VIDP_* / VIDP_* env vars.")
|
|
146
|
+
raise typer.Exit(1) from None
|
|
147
|
+
except Exception as exc: # noqa: BLE001
|
|
148
|
+
out.output_error(str(exc))
|
|
149
|
+
raise typer.Exit(1) from None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
workflows_app = typer.Typer(
|
|
153
|
+
name="workflows",
|
|
154
|
+
help=(
|
|
155
|
+
"Manage workflow alias registry. Package defaults ship with vds-scripts "
|
|
156
|
+
"(read-only); add/remove writes to the user override file "
|
|
157
|
+
"(~/.vds/vidp-workflows.json or $VDS_VIDP_WORKFLOWS_FILE)."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
app.add_typer(workflows_app, name="workflows")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@workflows_app.command("list")
|
|
164
|
+
def workflows_list(ctx: typer.Context) -> None:
|
|
165
|
+
"""List alias → workflow-id mappings, separated by source."""
|
|
166
|
+
out = OutputManager(CLIContext.from_typer_context(ctx))
|
|
167
|
+
package = load_package_aliases()
|
|
168
|
+
user = load_user_aliases()
|
|
169
|
+
overridden = {k for k in user if k in package and user[k] != package[k]}
|
|
170
|
+
out.output_json(
|
|
171
|
+
{
|
|
172
|
+
"packageFile": str(package_workflows_file()),
|
|
173
|
+
"userFile": str(user_workflows_file()),
|
|
174
|
+
"package": package,
|
|
175
|
+
"user": user,
|
|
176
|
+
"userOverridesPackage": sorted(overridden),
|
|
177
|
+
"merged": load_aliases(),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@workflows_app.command("add")
|
|
183
|
+
def workflows_add(
|
|
184
|
+
ctx: typer.Context,
|
|
185
|
+
name: Annotated[str, typer.Argument(help="Alias name (e.g. 'build-jenkins-by-job-name')")],
|
|
186
|
+
workflow_id: Annotated[str, typer.Argument(help="Workflow UUID")],
|
|
187
|
+
force: Annotated[
|
|
188
|
+
bool,
|
|
189
|
+
typer.Option("--force", help="Overwrite if alias already exists in user file."),
|
|
190
|
+
] = False,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Add or update an alias in the USER override file.
|
|
193
|
+
|
|
194
|
+
Package-shipped aliases are read-only; use ``--force`` to override them
|
|
195
|
+
locally via the user file.
|
|
196
|
+
"""
|
|
197
|
+
out = OutputManager(CLIContext.from_typer_context(ctx))
|
|
198
|
+
user = load_user_aliases()
|
|
199
|
+
package = load_package_aliases()
|
|
200
|
+
if name in user and not force:
|
|
201
|
+
out.output_error(
|
|
202
|
+
f"Alias '{name}' already in user file → '{user[name]}'. Use --force to overwrite."
|
|
203
|
+
)
|
|
204
|
+
raise typer.Exit(1) from None
|
|
205
|
+
user[name] = workflow_id
|
|
206
|
+
path = save_user_aliases(user)
|
|
207
|
+
out.output_json(
|
|
208
|
+
{
|
|
209
|
+
"saved": str(path),
|
|
210
|
+
"alias": name,
|
|
211
|
+
"workflowId": workflow_id,
|
|
212
|
+
"overridesPackage": name in package and package[name] != workflow_id,
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@workflows_app.command("remove")
|
|
218
|
+
def workflows_remove(
|
|
219
|
+
ctx: typer.Context,
|
|
220
|
+
name: Annotated[str, typer.Argument(help="Alias name to remove from user file")],
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Remove an alias from the USER override file.
|
|
223
|
+
|
|
224
|
+
Package-shipped aliases cannot be removed via CLI — edit the package
|
|
225
|
+
workflows.json directly to change the curated catalogue.
|
|
226
|
+
"""
|
|
227
|
+
out = OutputManager(CLIContext.from_typer_context(ctx))
|
|
228
|
+
user = load_user_aliases()
|
|
229
|
+
if name not in user:
|
|
230
|
+
package = load_package_aliases()
|
|
231
|
+
if name in package:
|
|
232
|
+
out.output_error(
|
|
233
|
+
f"Alias '{name}' is shipped with the package (read-only). "
|
|
234
|
+
f"Edit {package_workflows_file()} to change it, or use "
|
|
235
|
+
f"'workflows add {name} <other-uuid> --force' to override locally."
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
out.output_error(f"Alias '{name}' not found.")
|
|
239
|
+
raise typer.Exit(1) from None
|
|
240
|
+
removed = user.pop(name)
|
|
241
|
+
path = save_user_aliases(user)
|
|
242
|
+
out.output_json({"saved": str(path), "removed": name, "wasMappedTo": removed})
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
app()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""HTTP client for the VIDP third-party gateway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from vds_platform_core.http.errors import map_http_error
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from vds_vidp_orchestrator.config import VidpSettings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VidpClient:
|
|
16
|
+
"""Thin synchronous client around the two VIDP endpoints we expose."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings: VidpSettings) -> None:
|
|
19
|
+
self.settings = settings
|
|
20
|
+
self._client: httpx.Client | None = None
|
|
21
|
+
|
|
22
|
+
def _get_client(self) -> httpx.Client:
|
|
23
|
+
if self._client is None:
|
|
24
|
+
self._client = httpx.Client(
|
|
25
|
+
base_url=self.settings.normalized_base_url,
|
|
26
|
+
headers={"Accept": "application/json", **self.settings.auth_header},
|
|
27
|
+
timeout=self.settings.timeout_seconds,
|
|
28
|
+
)
|
|
29
|
+
return self._client
|
|
30
|
+
|
|
31
|
+
def _request(
|
|
32
|
+
self,
|
|
33
|
+
method: str,
|
|
34
|
+
path: str,
|
|
35
|
+
*,
|
|
36
|
+
json_body: Any | None = None,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
client = self._get_client()
|
|
39
|
+
response = client.request(method, path, json=json_body)
|
|
40
|
+
try:
|
|
41
|
+
data: Any = response.json()
|
|
42
|
+
except ValueError:
|
|
43
|
+
data = {"raw": response.text}
|
|
44
|
+
|
|
45
|
+
if not response.is_success:
|
|
46
|
+
raise map_http_error(response.status_code, data, service="vidp")
|
|
47
|
+
return data if isinstance(data, dict) else {"data": data}
|
|
48
|
+
|
|
49
|
+
def trigger(self, workflow_id: str, payload: Any | None = None) -> dict[str, Any]:
|
|
50
|
+
"""Trigger a third-party action by workflow id.
|
|
51
|
+
|
|
52
|
+
Mirrors ``POST /third-party/v2/start/{workflowId}``.
|
|
53
|
+
"""
|
|
54
|
+
body = payload if payload is not None else {}
|
|
55
|
+
path = f"third-party/v2/start/{quote(workflow_id, safe='')}"
|
|
56
|
+
return self._request("POST", path, json_body=body)
|
|
57
|
+
|
|
58
|
+
def status(self, job_id: str) -> dict[str, Any]:
|
|
59
|
+
"""Fetch full workflow/stage status for a VIDP job.
|
|
60
|
+
|
|
61
|
+
Mirrors ``GET /third-party/v2/status/{jobId}?full=true``.
|
|
62
|
+
"""
|
|
63
|
+
path = f"third-party/v2/status/{quote(job_id, safe='')}?full=true"
|
|
64
|
+
return self._request("GET", path)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def normalize_status(raw: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
+
"""Flatten the nested status response into the shape the original MCP returned."""
|
|
69
|
+
data = raw.get("data") or {}
|
|
70
|
+
workflows_raw = data.get("workflows") or []
|
|
71
|
+
workflows = []
|
|
72
|
+
for wf in workflows_raw:
|
|
73
|
+
steps = (wf or {}).get("steps") or {}
|
|
74
|
+
stages_raw = steps.get("stages") or []
|
|
75
|
+
workflows.append(
|
|
76
|
+
{
|
|
77
|
+
"name": (wf or {}).get("name"),
|
|
78
|
+
"status": (wf or {}).get("status"),
|
|
79
|
+
"stepStatus": steps.get("status"),
|
|
80
|
+
"stages": [
|
|
81
|
+
{"name": (s or {}).get("name"), "status": (s or {}).get("status")}
|
|
82
|
+
for s in stages_raw
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return {
|
|
87
|
+
"jobName": data.get("name"),
|
|
88
|
+
"jobStatus": data.get("status"),
|
|
89
|
+
"workflows": workflows,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
if self._client is not None:
|
|
94
|
+
self._client.close()
|
|
95
|
+
self._client = None
|
|
96
|
+
|
|
97
|
+
def __enter__(self) -> VidpClient:
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, *_: object) -> None:
|
|
101
|
+
self.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = ["VidpClient"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Strongly-typed VIDP settings using pydantic-settings.
|
|
2
|
+
|
|
3
|
+
Loads from the shared VDS env file (~/.vds/.env) with prefix `VDS_VIDP_`.
|
|
4
|
+
Accepts legacy `VIDP_*` env variables as aliases so existing VIDP MCP
|
|
5
|
+
deployments keep working without re-configuring credentials.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import AliasChoices, Field, SecretStr, ValidationError
|
|
15
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
16
|
+
from vds_cli_common.env import load_shared_env
|
|
17
|
+
from vds_platform_core.credentials import resolve_secret
|
|
18
|
+
from vds_platform_core.errors import SettingsError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VidpSettings(BaseSettings):
|
|
22
|
+
"""VIDP gateway settings."""
|
|
23
|
+
|
|
24
|
+
model_config = SettingsConfigDict(
|
|
25
|
+
env_file=None,
|
|
26
|
+
env_file_encoding="utf-8",
|
|
27
|
+
case_sensitive=True,
|
|
28
|
+
extra="ignore",
|
|
29
|
+
populate_by_name=False,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
base_url: str = Field(
|
|
33
|
+
validation_alias=AliasChoices("VDS_VIDP_BASE_URL", "VIDP_BASE_URL"),
|
|
34
|
+
description="VIDP base URL (e.g. https://idp.example.com/)",
|
|
35
|
+
)
|
|
36
|
+
username: str = Field(
|
|
37
|
+
validation_alias=AliasChoices("VDS_VIDP_USERNAME", "VIDP_USERNAME"),
|
|
38
|
+
description="Basic-auth username",
|
|
39
|
+
)
|
|
40
|
+
password_secret: SecretStr = Field(
|
|
41
|
+
validation_alias=AliasChoices("VDS_VIDP_PASSWORD", "VIDP_PASSWORD"),
|
|
42
|
+
description="Basic-auth password",
|
|
43
|
+
)
|
|
44
|
+
timeout_ms: int = Field(
|
|
45
|
+
default=30_000,
|
|
46
|
+
ge=1,
|
|
47
|
+
validation_alias=AliasChoices("VDS_VIDP_TIMEOUT_MS", "VIDP_TIMEOUT_MS"),
|
|
48
|
+
description="HTTP request timeout in milliseconds",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def password(self) -> str:
|
|
53
|
+
return resolve_secret(self.password_secret) or ""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def timeout_seconds(self) -> float:
|
|
57
|
+
return self.timeout_ms / 1000.0
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def normalized_base_url(self) -> str:
|
|
61
|
+
return self.base_url.rstrip("/") + "/"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def auth_header(self) -> dict[str, str]:
|
|
65
|
+
token = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
|
|
66
|
+
return {"Authorization": f"Basic {token}"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_settings(
|
|
70
|
+
*,
|
|
71
|
+
env_path: Path | None = None,
|
|
72
|
+
overrides: dict[str, Any] | None = None,
|
|
73
|
+
) -> VidpSettings:
|
|
74
|
+
"""Load VIDP settings from shared env + environment variables."""
|
|
75
|
+
load_shared_env(path=env_path)
|
|
76
|
+
try:
|
|
77
|
+
return VidpSettings(**(overrides or {}))
|
|
78
|
+
except ValidationError as exc:
|
|
79
|
+
raise SettingsError(str(exc)) from exc
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["VidpSettings", "load_settings", "SettingsError"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Workflow alias registry — map friendly names to VIDP workflow UUIDs.
|
|
2
|
+
|
|
3
|
+
Two layers, merged at lookup time:
|
|
4
|
+
|
|
5
|
+
1. **Package-shipped defaults** at ``<pkg>/workflows.json`` — curated by the
|
|
6
|
+
maintainer, distributed with the npm package, read-only at runtime.
|
|
7
|
+
2. **User overrides** at ``~/.vds/vidp-workflows.json`` (or
|
|
8
|
+
``$VDS_VIDP_WORKFLOWS_FILE``) — optional, user-editable, takes precedence
|
|
9
|
+
over package defaults. CLI ``add``/``remove`` only touch this file.
|
|
10
|
+
|
|
11
|
+
``resolve_workflow_id()`` accepts a UUID or an alias and returns the canonical
|
|
12
|
+
UUID. Alias lookup is case-insensitive.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def package_workflows_file() -> Path:
|
|
26
|
+
"""Path to the package-shipped alias registry (read-only)."""
|
|
27
|
+
return Path(__file__).resolve().parent / "workflows.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def user_workflows_file() -> Path:
|
|
31
|
+
"""Path to the user-override alias registry (mutable; CLI writes here)."""
|
|
32
|
+
override = os.environ.get("VDS_VIDP_WORKFLOWS_FILE", "").strip()
|
|
33
|
+
if override:
|
|
34
|
+
return Path(override).expanduser()
|
|
35
|
+
return Path.home() / ".vds" / "vidp-workflows.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Backwards-compatible name used by the CLI before the split.
|
|
39
|
+
def workflows_file() -> Path:
|
|
40
|
+
"""Deprecated alias for :func:`user_workflows_file`."""
|
|
41
|
+
return user_workflows_file()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_json(path: Path) -> dict[str, str]:
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
return {}
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
return {}
|
|
53
|
+
return {str(k).strip(): str(v).strip() for k, v in data.items() if isinstance(v, str)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_package_aliases() -> dict[str, str]:
|
|
57
|
+
"""Aliases shipped with the package."""
|
|
58
|
+
return _read_json(package_workflows_file())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_user_aliases() -> dict[str, str]:
|
|
62
|
+
"""Aliases set by the user (override layer)."""
|
|
63
|
+
return _read_json(user_workflows_file())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_aliases() -> dict[str, str]:
|
|
67
|
+
"""Merged view: package defaults overlaid with user overrides."""
|
|
68
|
+
merged = load_package_aliases()
|
|
69
|
+
merged.update(load_user_aliases())
|
|
70
|
+
return merged
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_user_aliases(aliases: dict[str, str]) -> Path:
|
|
74
|
+
"""Write the user-override registry, creating parent dir if needed."""
|
|
75
|
+
path = user_workflows_file()
|
|
76
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
path.write_text(json.dumps(aliases, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Backwards-compatible name used by older callers.
|
|
82
|
+
def save_aliases(aliases: dict[str, str]) -> Path:
|
|
83
|
+
"""Deprecated alias for :func:`save_user_aliases`."""
|
|
84
|
+
return save_user_aliases(aliases)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_uuid(value: str) -> bool:
|
|
88
|
+
return bool(UUID_RE.match(value.strip()))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def resolve_workflow_id(name_or_id: str) -> tuple[str, str | None]:
|
|
92
|
+
"""Return ``(workflow_id, alias_used)``.
|
|
93
|
+
|
|
94
|
+
- UUID input → returned as-is with ``alias_used=None``.
|
|
95
|
+
- Otherwise look up case-insensitively in the merged registry.
|
|
96
|
+
- Raises ``KeyError`` with a helpful message if no match.
|
|
97
|
+
"""
|
|
98
|
+
value = name_or_id.strip()
|
|
99
|
+
if is_uuid(value):
|
|
100
|
+
return value, None
|
|
101
|
+
|
|
102
|
+
aliases = load_aliases()
|
|
103
|
+
if value in aliases:
|
|
104
|
+
return aliases[value], value
|
|
105
|
+
lower_map = {k.lower(): (k, v) for k, v in aliases.items()}
|
|
106
|
+
if value.lower() in lower_map:
|
|
107
|
+
canonical, wf_id = lower_map[value.lower()]
|
|
108
|
+
return wf_id, canonical
|
|
109
|
+
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f"No workflow id found for alias '{value}'. "
|
|
112
|
+
f"Maintainer can add it to {package_workflows_file()} "
|
|
113
|
+
f"(shipped with package); user can add a private alias via "
|
|
114
|
+
f"'vds-cli vidp workflows add {value} <uuid>' "
|
|
115
|
+
f"(writes to {user_workflows_file()})."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
"package_workflows_file",
|
|
121
|
+
"user_workflows_file",
|
|
122
|
+
"workflows_file", # backwards-compat
|
|
123
|
+
"load_package_aliases",
|
|
124
|
+
"load_user_aliases",
|
|
125
|
+
"load_aliases",
|
|
126
|
+
"save_user_aliases",
|
|
127
|
+
"save_aliases", # backwards-compat
|
|
128
|
+
"is_uuid",
|
|
129
|
+
"resolve_workflow_id",
|
|
130
|
+
]
|