dwf-platform-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dwf_cli/__init__.py +3 -0
- dwf_cli/__main__.py +4 -0
- dwf_cli/api/__init__.py +3 -0
- dwf_cli/api/auth.py +46 -0
- dwf_cli/api/client.py +113 -0
- dwf_cli/api/datamodel.py +458 -0
- dwf_cli/api/formmodel.py +197 -0
- dwf_cli/api/funcmodel.py +436 -0
- dwf_cli/cli/__init__.py +69 -0
- dwf_cli/cli/_common.py +152 -0
- dwf_cli/cli/auth.py +197 -0
- dwf_cli/cli/config.py +200 -0
- dwf_cli/cli/datamodel.py +2270 -0
- dwf_cli/cli/formmodel.py +1007 -0
- dwf_cli/cli/funcmodel.py +2055 -0
- dwf_cli/cli/schema.py +210 -0
- dwf_cli/core/__init__.py +0 -0
- dwf_cli/core/config.py +177 -0
- dwf_cli/core/crypto.py +22 -0
- dwf_cli/core/errors.py +63 -0
- dwf_cli/core/output.py +6 -0
- dwf_cli/core/validator.py +129 -0
- dwf_cli/mcp/__init__.py +3 -0
- dwf_cli/mcp/server.py +411 -0
- dwf_cli/schemas/__init__.py +37 -0
- dwf_cli/schemas/datamodel/attribute_bind.schema.json +21 -0
- dwf_cli/schemas/datamodel/attribute_create.schema.json +24 -0
- dwf_cli/schemas/datamodel/attribute_update.schema.json +21 -0
- dwf_cli/schemas/datamodel/create.schema.json +27 -0
- dwf_cli/schemas/datamodel/excel_confirm.schema.json +65 -0
- dwf_cli/schemas/datamodel/external_create.schema.json +47 -0
- dwf_cli/schemas/datamodel/external_update.schema.json +47 -0
- dwf_cli/schemas/datamodel/object_create.schema.json +24 -0
- dwf_cli/schemas/datamodel/object_update.schema.json +17 -0
- dwf_cli/schemas/datamodel/relation_create.schema.json +34 -0
- dwf_cli/schemas/datamodel/relation_update.schema.json +34 -0
- dwf_cli/schemas/datamodel/update.schema.json +26 -0
- dwf_cli/schemas/funcmodel/app_create.schema.json +150 -0
- dwf_cli/schemas/funcmodel/app_update.schema.json +153 -0
- dwf_cli/schemas/funcmodel/language-package_create.schema.json +15 -0
- dwf_cli/schemas/funcmodel/operations_create.schema.json +77 -0
- dwf_cli/schemas/funcmodel/operations_update.schema.json +76 -0
- dwf_platform_cli-0.2.0.dist-info/METADATA +347 -0
- dwf_platform_cli-0.2.0.dist-info/RECORD +47 -0
- dwf_platform_cli-0.2.0.dist-info/WHEEL +4 -0
- dwf_platform_cli-0.2.0.dist-info/entry_points.txt +3 -0
- dwf_platform_cli-0.2.0.dist-info/licenses/LICENSE +190 -0
dwf_cli/cli/_common.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as json_mod
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from dwf_cli.api.client import APIClient
|
|
14
|
+
from dwf_cli.core.config import Config
|
|
15
|
+
from dwf_cli.core.errors import AuthError, DWFError
|
|
16
|
+
from dwf_cli.core.output import OutputFormat
|
|
17
|
+
|
|
18
|
+
_SENSITIVE_DIRS = {".ssh", ".gnupg", ".aws", ".kube"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ContextObj:
|
|
23
|
+
config: Config
|
|
24
|
+
client: APIClient | None
|
|
25
|
+
app_client: APIClient | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SuccessResult:
|
|
30
|
+
action: str
|
|
31
|
+
resource: str
|
|
32
|
+
detail: str | None = None
|
|
33
|
+
data: Any = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def report_success(
|
|
37
|
+
result: SuccessResult, fmt: OutputFormat = OutputFormat.table
|
|
38
|
+
) -> None:
|
|
39
|
+
fmt = resolve_format(fmt)
|
|
40
|
+
if fmt == OutputFormat.json:
|
|
41
|
+
obj: dict[str, Any] = {
|
|
42
|
+
"success": True,
|
|
43
|
+
"action": result.action,
|
|
44
|
+
"resource": result.resource,
|
|
45
|
+
}
|
|
46
|
+
if result.detail:
|
|
47
|
+
obj["detail"] = result.detail
|
|
48
|
+
if result.data is not None:
|
|
49
|
+
obj["data"] = result.data
|
|
50
|
+
typer.echo(json_mod.dumps(obj, ensure_ascii=False, indent=2))
|
|
51
|
+
else:
|
|
52
|
+
msg = f"{result.action} {result.resource}"
|
|
53
|
+
if result.detail:
|
|
54
|
+
msg += f": {result.detail}"
|
|
55
|
+
Console().print(f"[green]{msg}[/green]")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_tty() -> bool:
|
|
59
|
+
return sys.stdout.isatty()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _should_disable_color() -> bool:
|
|
63
|
+
if os.environ.get("NO_COLOR") is not None:
|
|
64
|
+
return True
|
|
65
|
+
if os.environ.get("TERM") == "dumb":
|
|
66
|
+
return True
|
|
67
|
+
if not is_tty():
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_console: Console | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_console() -> Console:
|
|
76
|
+
global _console
|
|
77
|
+
if _console is None:
|
|
78
|
+
_console = Console(no_color=_should_disable_color())
|
|
79
|
+
return _console
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolve_format(fmt: OutputFormat) -> OutputFormat:
|
|
83
|
+
if fmt == OutputFormat.table and not is_tty():
|
|
84
|
+
return OutputFormat.json
|
|
85
|
+
return fmt
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_path(path: Path) -> Path:
|
|
89
|
+
resolved = path.resolve()
|
|
90
|
+
for part in resolved.parts:
|
|
91
|
+
if part in _SENSITIVE_DIRS:
|
|
92
|
+
raise DWFError(f"Path contains sensitive directory: {part}")
|
|
93
|
+
if ".." in str(path):
|
|
94
|
+
raise DWFError("Path traversal not allowed")
|
|
95
|
+
return resolved
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_client(ctx: typer.Context) -> APIClient:
|
|
99
|
+
obj: ContextObj = ctx.obj
|
|
100
|
+
if obj.client is None:
|
|
101
|
+
raise AuthError("Not logged in")
|
|
102
|
+
return obj.client
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_app_client(ctx: typer.Context) -> APIClient:
|
|
106
|
+
obj: ContextObj = ctx.obj
|
|
107
|
+
if obj.app_client is not None:
|
|
108
|
+
return obj.app_client
|
|
109
|
+
if obj.client is not None:
|
|
110
|
+
return obj.client
|
|
111
|
+
raise AuthError("Not logged in")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def handle_error(exc: DWFError) -> None:
|
|
115
|
+
if is_tty():
|
|
116
|
+
parts = [f"Error: {exc}"]
|
|
117
|
+
if exc.suggestion:
|
|
118
|
+
parts.append(f" 建议: {exc.suggestion}")
|
|
119
|
+
if exc.detail:
|
|
120
|
+
parts.append(f" 详情: {exc.detail}")
|
|
121
|
+
typer.echo("\n".join(parts), err=True)
|
|
122
|
+
else:
|
|
123
|
+
_print_error_json(
|
|
124
|
+
type(exc).__name__,
|
|
125
|
+
str(exc),
|
|
126
|
+
exc.suggestion,
|
|
127
|
+
retryable=exc.retryable,
|
|
128
|
+
detail=exc.detail,
|
|
129
|
+
)
|
|
130
|
+
raise typer.Exit(code=exc.exit_code)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _print_error_json(
|
|
134
|
+
error_type: str,
|
|
135
|
+
message: str,
|
|
136
|
+
suggestion: str | None,
|
|
137
|
+
retryable: bool = False,
|
|
138
|
+
*,
|
|
139
|
+
detail: str | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
import json as json_mod
|
|
142
|
+
|
|
143
|
+
error_obj: dict[str, Any] = {
|
|
144
|
+
"error": error_type,
|
|
145
|
+
"message": message,
|
|
146
|
+
"retryable": retryable,
|
|
147
|
+
}
|
|
148
|
+
if suggestion:
|
|
149
|
+
error_obj["suggestion"] = suggestion
|
|
150
|
+
if detail:
|
|
151
|
+
error_obj["detail"] = detail
|
|
152
|
+
typer.echo(json_mod.dumps(error_obj, ensure_ascii=False), err=True)
|
dwf_cli/cli/auth.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as json_mod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from dwf_cli.api import auth as auth_api
|
|
10
|
+
from dwf_cli.api.client import APIClient
|
|
11
|
+
from dwf_cli.cli._common import (
|
|
12
|
+
ContextObj,
|
|
13
|
+
handle_error,
|
|
14
|
+
is_tty,
|
|
15
|
+
resolve_format,
|
|
16
|
+
validate_path,
|
|
17
|
+
)
|
|
18
|
+
from dwf_cli.core.errors import AuthError, ConfigError, DWFError
|
|
19
|
+
from dwf_cli.core.output import OutputFormat
|
|
20
|
+
|
|
21
|
+
_HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
22
|
+
|
|
23
|
+
DRY_RUN_EXIT_CODE = 10
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help="User authentication.\n\n"
|
|
27
|
+
"Examples:\n"
|
|
28
|
+
" dwf-cli auth login --username admin --password-file /path/to/password\n"
|
|
29
|
+
" dwf-cli auth login --username admin (prompts for password in TTY)\n"
|
|
30
|
+
" dwf-cli auth status\n"
|
|
31
|
+
" dwf-cli auth logout",
|
|
32
|
+
context_settings=_HELP_CONTEXT_SETTINGS,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def login(
|
|
38
|
+
ctx: typer.Context,
|
|
39
|
+
username: Annotated[
|
|
40
|
+
str,
|
|
41
|
+
typer.Option(
|
|
42
|
+
prompt=is_tty(),
|
|
43
|
+
help="Username (required; will prompt in TTY mode)",
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
password_file: Annotated[
|
|
47
|
+
Path | None,
|
|
48
|
+
typer.Option(
|
|
49
|
+
"--password-file",
|
|
50
|
+
help="Read password from file (first line, trailing newline stripped)",
|
|
51
|
+
),
|
|
52
|
+
] = None,
|
|
53
|
+
dry_run: Annotated[
|
|
54
|
+
bool,
|
|
55
|
+
typer.Option("--dry-run", help="Preview without executing"),
|
|
56
|
+
] = False,
|
|
57
|
+
) -> None:
|
|
58
|
+
try:
|
|
59
|
+
password = ""
|
|
60
|
+
if password_file:
|
|
61
|
+
password_file = validate_path(password_file)
|
|
62
|
+
password = password_file.read_text(encoding="utf-8").splitlines()[0]
|
|
63
|
+
elif is_tty():
|
|
64
|
+
password = typer.prompt("Password", hide_input=True)
|
|
65
|
+
else:
|
|
66
|
+
raise AuthError(
|
|
67
|
+
"Password required in non-TTY mode",
|
|
68
|
+
detail="Use --password-file or run in TTY mode",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
obj: ContextObj = ctx.obj
|
|
72
|
+
server = obj.config.server
|
|
73
|
+
if not server:
|
|
74
|
+
raise ConfigError(
|
|
75
|
+
"Server not configured",
|
|
76
|
+
detail="Run 'dwf-cli config set server <url>' first",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if dry_run:
|
|
80
|
+
typer.echo(
|
|
81
|
+
json_mod.dumps(
|
|
82
|
+
{
|
|
83
|
+
"action": "login",
|
|
84
|
+
"resource": "auth",
|
|
85
|
+
"server": server,
|
|
86
|
+
"username": username,
|
|
87
|
+
"reversible": True,
|
|
88
|
+
},
|
|
89
|
+
ensure_ascii=False,
|
|
90
|
+
indent=2,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
raise typer.Exit(code=DRY_RUN_EXIT_CODE)
|
|
94
|
+
|
|
95
|
+
temp_client = APIClient(server)
|
|
96
|
+
token = auth_api.login(
|
|
97
|
+
temp_client,
|
|
98
|
+
username,
|
|
99
|
+
password,
|
|
100
|
+
aes_key=obj.config.aes_key,
|
|
101
|
+
aes_iv=obj.config.aes_iv,
|
|
102
|
+
)
|
|
103
|
+
obj.config.save_token(token)
|
|
104
|
+
typer.echo(f"Logged in to {server}", err=True)
|
|
105
|
+
except DWFError as exc:
|
|
106
|
+
handle_error(exc)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command()
|
|
110
|
+
def logout(
|
|
111
|
+
ctx: typer.Context,
|
|
112
|
+
dry_run: Annotated[
|
|
113
|
+
bool,
|
|
114
|
+
typer.Option("--dry-run", help="Preview without executing"),
|
|
115
|
+
] = False,
|
|
116
|
+
) -> None:
|
|
117
|
+
try:
|
|
118
|
+
obj: ContextObj = ctx.obj
|
|
119
|
+
if dry_run:
|
|
120
|
+
typer.echo(
|
|
121
|
+
json_mod.dumps(
|
|
122
|
+
{
|
|
123
|
+
"action": "logout",
|
|
124
|
+
"resource": "auth",
|
|
125
|
+
"has_token": bool(obj.config.token),
|
|
126
|
+
"reversible": True,
|
|
127
|
+
},
|
|
128
|
+
ensure_ascii=False,
|
|
129
|
+
indent=2,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
raise typer.Exit(code=DRY_RUN_EXIT_CODE)
|
|
133
|
+
|
|
134
|
+
obj.config.clear_token()
|
|
135
|
+
typer.echo("Logged out.", err=True)
|
|
136
|
+
except DWFError as exc:
|
|
137
|
+
handle_error(exc)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def status(
|
|
142
|
+
ctx: typer.Context,
|
|
143
|
+
fmt: Annotated[
|
|
144
|
+
OutputFormat,
|
|
145
|
+
typer.Option("--format", "-f", help="Output format: table/json"),
|
|
146
|
+
] = OutputFormat.table,
|
|
147
|
+
) -> None:
|
|
148
|
+
obj: ContextObj = ctx.obj
|
|
149
|
+
fmt = resolve_format(fmt)
|
|
150
|
+
|
|
151
|
+
if not obj.config.token:
|
|
152
|
+
if fmt == OutputFormat.json:
|
|
153
|
+
typer.echo(json_mod.dumps({"logged_in": False}))
|
|
154
|
+
else:
|
|
155
|
+
typer.echo("Not logged in.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if obj.client is None:
|
|
159
|
+
if fmt == OutputFormat.json:
|
|
160
|
+
typer.echo(json_mod.dumps({"logged_in": False, "reason": "no_server"}))
|
|
161
|
+
else:
|
|
162
|
+
typer.echo("Not logged in (no server configured).")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
valid = auth_api.validate_token(obj.client)
|
|
167
|
+
if valid:
|
|
168
|
+
if fmt == OutputFormat.json:
|
|
169
|
+
typer.echo(
|
|
170
|
+
json_mod.dumps(
|
|
171
|
+
{"logged_in": True, "server": obj.config.server},
|
|
172
|
+
ensure_ascii=False,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
typer.echo(f"Logged in ({obj.config.server})")
|
|
177
|
+
else:
|
|
178
|
+
if fmt == OutputFormat.json:
|
|
179
|
+
typer.echo(
|
|
180
|
+
json_mod.dumps(
|
|
181
|
+
{
|
|
182
|
+
"logged_in": False,
|
|
183
|
+
"server": obj.config.server,
|
|
184
|
+
"reason": "token_expired",
|
|
185
|
+
},
|
|
186
|
+
ensure_ascii=False,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
typer.echo("Token expired. Run 'dwf-cli auth login' again.")
|
|
191
|
+
except DWFError:
|
|
192
|
+
if fmt == OutputFormat.json:
|
|
193
|
+
typer.echo(
|
|
194
|
+
json_mod.dumps({"logged_in": False, "reason": "server_unreachable"})
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
typer.echo("Token expired or server unreachable.")
|
dwf_cli/cli/config.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as json_mod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from dwf_cli.cli._common import ContextObj, handle_error, resolve_format
|
|
11
|
+
from dwf_cli.core.config import parse_config_js, resolve_config_js_url
|
|
12
|
+
from dwf_cli.core.errors import ConfigError, DWFError
|
|
13
|
+
from dwf_cli.core.output import OutputFormat
|
|
14
|
+
|
|
15
|
+
_HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
16
|
+
|
|
17
|
+
DRY_RUN_EXIT_CODE = 10
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
help="CLI configuration.\n\n"
|
|
21
|
+
"Examples:\n"
|
|
22
|
+
" dwf-cli config set server https://your-dwf-server.com\n"
|
|
23
|
+
" dwf-cli config get server\n"
|
|
24
|
+
" dwf-cli config list\n"
|
|
25
|
+
" dwf-cli config detect http://host:6060 --save",
|
|
26
|
+
context_settings=_HELP_CONTEXT_SETTINGS,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command(name="list")
|
|
31
|
+
def list_config(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
fmt: Annotated[
|
|
34
|
+
OutputFormat,
|
|
35
|
+
typer.Option("--format", "-f", help="Output format: table/json"),
|
|
36
|
+
] = OutputFormat.table,
|
|
37
|
+
) -> None:
|
|
38
|
+
try:
|
|
39
|
+
obj: ContextObj = ctx.obj
|
|
40
|
+
data = obj.config.list_all()
|
|
41
|
+
fmt = resolve_format(fmt)
|
|
42
|
+
|
|
43
|
+
if fmt == OutputFormat.json:
|
|
44
|
+
typer.echo(json_mod.dumps(data, ensure_ascii=False, indent=2))
|
|
45
|
+
else:
|
|
46
|
+
for key, value in data.items():
|
|
47
|
+
typer.echo(f" {key} = {value}")
|
|
48
|
+
except DWFError as exc:
|
|
49
|
+
handle_error(exc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def get(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
key: Annotated[str, typer.Argument(help="Config key")],
|
|
56
|
+
fmt: Annotated[
|
|
57
|
+
OutputFormat,
|
|
58
|
+
typer.Option("--format", "-f", help="Output format: table/json"),
|
|
59
|
+
] = OutputFormat.table,
|
|
60
|
+
) -> None:
|
|
61
|
+
try:
|
|
62
|
+
obj: ContextObj = ctx.obj
|
|
63
|
+
value = obj.config.get(key)
|
|
64
|
+
if value is None:
|
|
65
|
+
raise ConfigError(f"Key not found: {key}")
|
|
66
|
+
|
|
67
|
+
fmt = resolve_format(fmt)
|
|
68
|
+
|
|
69
|
+
if fmt == OutputFormat.json:
|
|
70
|
+
typer.echo(json_mod.dumps({key: value}, ensure_ascii=False, indent=2))
|
|
71
|
+
else:
|
|
72
|
+
typer.echo(f"{key} = {value}")
|
|
73
|
+
except DWFError as exc:
|
|
74
|
+
handle_error(exc)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def set(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
key: Annotated[str, typer.Argument(help="Config key")],
|
|
81
|
+
value: Annotated[str, typer.Argument(help="Config value")],
|
|
82
|
+
dry_run: Annotated[
|
|
83
|
+
bool,
|
|
84
|
+
typer.Option("--dry-run", help="Preview without executing"),
|
|
85
|
+
] = False,
|
|
86
|
+
) -> None:
|
|
87
|
+
try:
|
|
88
|
+
if dry_run:
|
|
89
|
+
typer.echo(
|
|
90
|
+
json_mod.dumps(
|
|
91
|
+
{
|
|
92
|
+
"action": "set",
|
|
93
|
+
"resource": "config",
|
|
94
|
+
"key": key,
|
|
95
|
+
"value": value,
|
|
96
|
+
"reversible": True,
|
|
97
|
+
},
|
|
98
|
+
ensure_ascii=False,
|
|
99
|
+
indent=2,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
raise typer.Exit(code=DRY_RUN_EXIT_CODE)
|
|
103
|
+
|
|
104
|
+
obj: ContextObj = ctx.obj
|
|
105
|
+
obj.config.set(key, value)
|
|
106
|
+
typer.echo(f"Set {key} = {value}", err=True)
|
|
107
|
+
except DWFError as exc:
|
|
108
|
+
handle_error(exc)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command()
|
|
112
|
+
def detect(
|
|
113
|
+
ctx: typer.Context,
|
|
114
|
+
source: Annotated[
|
|
115
|
+
str,
|
|
116
|
+
typer.Argument(
|
|
117
|
+
help="DWF server URL or local config.js path. "
|
|
118
|
+
"Examples: http://host:6060, http://host/modeler-web, ./config.js"
|
|
119
|
+
),
|
|
120
|
+
],
|
|
121
|
+
save: Annotated[
|
|
122
|
+
bool,
|
|
123
|
+
typer.Option("--save", help="Save detected config to config file"),
|
|
124
|
+
] = False,
|
|
125
|
+
dry_run: Annotated[
|
|
126
|
+
bool,
|
|
127
|
+
typer.Option("--dry-run", help="Preview without saving"),
|
|
128
|
+
] = False,
|
|
129
|
+
fmt: Annotated[
|
|
130
|
+
OutputFormat,
|
|
131
|
+
typer.Option("--format", "-f", help="Output format: table/json"),
|
|
132
|
+
] = OutputFormat.table,
|
|
133
|
+
) -> None:
|
|
134
|
+
try:
|
|
135
|
+
resolved = resolve_config_js_url(source)
|
|
136
|
+
content: str | None = None
|
|
137
|
+
actual_url: str | None = None
|
|
138
|
+
|
|
139
|
+
p = Path(resolved)
|
|
140
|
+
if p.exists() and p.is_file():
|
|
141
|
+
content = p.read_text(encoding="utf-8")
|
|
142
|
+
else:
|
|
143
|
+
actual_url = resolved
|
|
144
|
+
try:
|
|
145
|
+
resp = httpx.get(resolved, timeout=15.0, follow_redirects=True)
|
|
146
|
+
resp.raise_for_status()
|
|
147
|
+
content = resp.text
|
|
148
|
+
except httpx.HTTPError as exc:
|
|
149
|
+
err = ConfigError(f"Failed to fetch config.js from {resolved}: {exc}")
|
|
150
|
+
err.suggestion = "Check the URL or provide a local config.js file path"
|
|
151
|
+
raise err
|
|
152
|
+
|
|
153
|
+
detected = parse_config_js(content, source_url=actual_url)
|
|
154
|
+
if not detected:
|
|
155
|
+
err = ConfigError(f"No DWF config fields found in {resolved}")
|
|
156
|
+
err.suggestion = (
|
|
157
|
+
"Ensure the file contains modelerApiBase or appApiBase fields"
|
|
158
|
+
)
|
|
159
|
+
raise err
|
|
160
|
+
|
|
161
|
+
if dry_run or not save:
|
|
162
|
+
fmt = resolve_format(fmt)
|
|
163
|
+
if fmt == OutputFormat.json:
|
|
164
|
+
output: dict[str, Any] = {
|
|
165
|
+
"source": resolved,
|
|
166
|
+
"detected": detected,
|
|
167
|
+
"saved": False,
|
|
168
|
+
}
|
|
169
|
+
typer.echo(json_mod.dumps(output, ensure_ascii=False, indent=2))
|
|
170
|
+
else:
|
|
171
|
+
typer.echo(f"Detected from {resolved}:", err=True)
|
|
172
|
+
for key, value in detected.items():
|
|
173
|
+
typer.echo(f" {key} = {value}", err=True)
|
|
174
|
+
if not save and not dry_run:
|
|
175
|
+
typer.echo(" (use --save to write to config)", err=True)
|
|
176
|
+
if dry_run:
|
|
177
|
+
raise typer.Exit(code=DRY_RUN_EXIT_CODE)
|
|
178
|
+
else:
|
|
179
|
+
obj: ContextObj = ctx.obj
|
|
180
|
+
updated = obj.config.bulk_set(detected)
|
|
181
|
+
fmt = resolve_format(fmt)
|
|
182
|
+
if fmt == OutputFormat.json:
|
|
183
|
+
typer.echo(
|
|
184
|
+
json_mod.dumps(
|
|
185
|
+
{
|
|
186
|
+
"source": resolved,
|
|
187
|
+
"detected": detected,
|
|
188
|
+
"saved": True,
|
|
189
|
+
"updated_keys": updated,
|
|
190
|
+
},
|
|
191
|
+
ensure_ascii=False,
|
|
192
|
+
indent=2,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
typer.echo(f"Detected and saved from {resolved}:", err=True)
|
|
197
|
+
for key in updated:
|
|
198
|
+
typer.echo(f" {key} = {detected[key]}", err=True)
|
|
199
|
+
except DWFError as exc:
|
|
200
|
+
handle_error(exc)
|