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.
Files changed (47) hide show
  1. dwf_cli/__init__.py +3 -0
  2. dwf_cli/__main__.py +4 -0
  3. dwf_cli/api/__init__.py +3 -0
  4. dwf_cli/api/auth.py +46 -0
  5. dwf_cli/api/client.py +113 -0
  6. dwf_cli/api/datamodel.py +458 -0
  7. dwf_cli/api/formmodel.py +197 -0
  8. dwf_cli/api/funcmodel.py +436 -0
  9. dwf_cli/cli/__init__.py +69 -0
  10. dwf_cli/cli/_common.py +152 -0
  11. dwf_cli/cli/auth.py +197 -0
  12. dwf_cli/cli/config.py +200 -0
  13. dwf_cli/cli/datamodel.py +2270 -0
  14. dwf_cli/cli/formmodel.py +1007 -0
  15. dwf_cli/cli/funcmodel.py +2055 -0
  16. dwf_cli/cli/schema.py +210 -0
  17. dwf_cli/core/__init__.py +0 -0
  18. dwf_cli/core/config.py +177 -0
  19. dwf_cli/core/crypto.py +22 -0
  20. dwf_cli/core/errors.py +63 -0
  21. dwf_cli/core/output.py +6 -0
  22. dwf_cli/core/validator.py +129 -0
  23. dwf_cli/mcp/__init__.py +3 -0
  24. dwf_cli/mcp/server.py +411 -0
  25. dwf_cli/schemas/__init__.py +37 -0
  26. dwf_cli/schemas/datamodel/attribute_bind.schema.json +21 -0
  27. dwf_cli/schemas/datamodel/attribute_create.schema.json +24 -0
  28. dwf_cli/schemas/datamodel/attribute_update.schema.json +21 -0
  29. dwf_cli/schemas/datamodel/create.schema.json +27 -0
  30. dwf_cli/schemas/datamodel/excel_confirm.schema.json +65 -0
  31. dwf_cli/schemas/datamodel/external_create.schema.json +47 -0
  32. dwf_cli/schemas/datamodel/external_update.schema.json +47 -0
  33. dwf_cli/schemas/datamodel/object_create.schema.json +24 -0
  34. dwf_cli/schemas/datamodel/object_update.schema.json +17 -0
  35. dwf_cli/schemas/datamodel/relation_create.schema.json +34 -0
  36. dwf_cli/schemas/datamodel/relation_update.schema.json +34 -0
  37. dwf_cli/schemas/datamodel/update.schema.json +26 -0
  38. dwf_cli/schemas/funcmodel/app_create.schema.json +150 -0
  39. dwf_cli/schemas/funcmodel/app_update.schema.json +153 -0
  40. dwf_cli/schemas/funcmodel/language-package_create.schema.json +15 -0
  41. dwf_cli/schemas/funcmodel/operations_create.schema.json +77 -0
  42. dwf_cli/schemas/funcmodel/operations_update.schema.json +76 -0
  43. dwf_platform_cli-0.2.0.dist-info/METADATA +347 -0
  44. dwf_platform_cli-0.2.0.dist-info/RECORD +47 -0
  45. dwf_platform_cli-0.2.0.dist-info/WHEEL +4 -0
  46. dwf_platform_cli-0.2.0.dist-info/entry_points.txt +3 -0
  47. dwf_platform_cli-0.2.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,2055 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json as json_mod
5
+ import re
6
+ import secrets
7
+ import string as string_mod
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Annotated, Any
11
+
12
+ import pypinyin
13
+ import typer
14
+ from rich.table import Table
15
+
16
+ from dwf_cli.api import funcmodel as funcmodel_api
17
+ from dwf_cli.api.client import raise_if_not_success
18
+ from dwf_cli.cli._common import (
19
+ SuccessResult,
20
+ get_client,
21
+ get_console,
22
+ handle_error,
23
+ is_tty,
24
+ report_success,
25
+ resolve_format,
26
+ )
27
+ from dwf_cli.core.errors import DWFError
28
+ from dwf_cli.core.output import OutputFormat
29
+
30
+ _HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
31
+
32
+ app = typer.Typer(
33
+ help="Function model management.\n\n"
34
+ "Examples:\n"
35
+ " # App commands\n"
36
+ " dwf-cli funcmodel app list\n"
37
+ " dwf-cli funcmodel app get --name MyApp\n"
38
+ " dwf-cli funcmodel app create --title '我的应用'\n"
39
+ " dwf-cli funcmodel app create --title '设备管理' --type Mobile --app-name DeviceApp\n"
40
+ " dwf-cli funcmodel app create --data-file ./app.json\n"
41
+ " dwf-cli funcmodel app update --title '新名称' --data-file ./patch.json\n"
42
+ " dwf-cli funcmodel app delete --id app_abc123\n"
43
+ " dwf-cli funcmodel app upload-logo --id app_abc123 --file ./logo.png\n\n"
44
+ " # Theme commands\n"
45
+ " dwf-cli funcmodel theme list\n"
46
+ " dwf-cli funcmodel theme create --name my-theme --file ./style.css --note '注释'\n"
47
+ " dwf-cli funcmodel theme update THEME_OID --file ./new.css --name updated-theme\n\n"
48
+ " # Language package commands\n"
49
+ " dwf-cli funcmodel language-package list --app-id MyApp\n"
50
+ ' dwf-cli funcmodel language-package create --data \'"{\\"appId\\":\\"MyApp\\",\\"language\\":\\"zh_CN\\"}"\' --file ./zh_CN.json\n'
51
+ " dwf-cli funcmodel language-package update PKG_ID --file ./zh_CN_updated.json\n\n"
52
+ " # Operations commands\n"
53
+ " dwf-cli funcmodel operations list -t entity --search Animate\n"
54
+ " dwf-cli funcmodel operations list -t global -p 0 -s 10\n"
55
+ " dwf-cli funcmodel operations all\n"
56
+ " dwf-cli funcmodel operations keywords\n"
57
+ " dwf-cli funcmodel operations get OPR_OID\n"
58
+ " dwf-cli funcmodel operations get-by-ids --id OID1 --id OID2\n"
59
+ " dwf-cli funcmodel operations create --data-file ./operation.json\n"
60
+ " dwf-cli funcmodel operations update --data-file ./operation_update.json\n"
61
+ " dwf-cli funcmodel operations delete OPR_OID\n"
62
+ " dwf-cli funcmodel operations move OPR_OID --parent-id PARENT_OID\n"
63
+ " dwf-cli funcmodel operations hide OPR_OID --hide\n"
64
+ " dwf-cli funcmodel operations hide OPR_OID --show\n"
65
+ ' dwf-cli funcmodel operations check-name --data \'{"displayName":"测试"}\'\n'
66
+ " dwf-cli funcmodel operations global-scripts\n\n"
67
+ " # Module commands\n"
68
+ " dwf-cli funcmodel modules list --app-id MyApp\n"
69
+ " dwf-cli funcmodel modules list --with-default\n"
70
+ " dwf-cli funcmodel modules tree\n"
71
+ " dwf-cli funcmodel modules tree --app-id MyApp --with-hidden\n"
72
+ " dwf-cli funcmodel modules operations MyModule\n"
73
+ ' dwf-cli funcmodel modules update-order MyApp --data \'[{"id":"module1","className":"Module1","children":[]}]\'',
74
+ context_settings=_HELP_CONTEXT_SETTINGS,
75
+ )
76
+
77
+ _app_sub = typer.Typer(
78
+ help="Application management.", context_settings=_HELP_CONTEXT_SETTINGS
79
+ )
80
+ _theme_sub = typer.Typer(
81
+ help="Theme management.", context_settings=_HELP_CONTEXT_SETTINGS
82
+ )
83
+ _lang_pkg_sub = typer.Typer(
84
+ help="Language package management.", context_settings=_HELP_CONTEXT_SETTINGS
85
+ )
86
+ _operations_sub = typer.Typer(
87
+ help="Operations management.", context_settings=_HELP_CONTEXT_SETTINGS
88
+ )
89
+ _modules_sub = typer.Typer(
90
+ help="Module management.", context_settings=_HELP_CONTEXT_SETTINGS
91
+ )
92
+
93
+ app.add_typer(_app_sub, name="app")
94
+ app.add_typer(_theme_sub, name="theme")
95
+ app.add_typer(_lang_pkg_sub, name="language-package")
96
+ app.add_typer(_operations_sub, name="operations")
97
+ app.add_typer(_modules_sub, name="modules")
98
+
99
+
100
+ class OperationType(str, Enum):
101
+ global_ = "global"
102
+ entity = "entity"
103
+ relation = "relation"
104
+
105
+
106
+ # ── Shared helpers ────────────────────────────────────────────────────
107
+
108
+
109
+ def _resolve_data(data: str | None, data_file: Path | None) -> str:
110
+ if data_file:
111
+ return data_file.read_text(encoding="utf-8")
112
+ if data is not None:
113
+ return data
114
+ raise typer.BadParameter("Provide --data <json> or --data-file <path>")
115
+
116
+
117
+ def _extract_items(result: Any) -> list[dict[str, Any]]:
118
+ if isinstance(result, list):
119
+ return result
120
+ if isinstance(result, dict):
121
+ payload = result.get("data", result)
122
+ if isinstance(payload, dict):
123
+ items = payload.get("data", payload.get("items", []))
124
+ return items if isinstance(items, list) else []
125
+ if isinstance(payload, list):
126
+ return payload
127
+ return []
128
+
129
+
130
+ def _flatten_operations(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
131
+ flat: list[dict[str, Any]] = []
132
+ for item in items:
133
+ if not isinstance(item, dict):
134
+ flat.append(item)
135
+ continue
136
+ is_grouped = True
137
+ for v in item.values():
138
+ if not isinstance(v, list):
139
+ is_grouped = False
140
+ break
141
+ if is_grouped and item:
142
+ for class_name, ops in item.items():
143
+ if isinstance(ops, list):
144
+ for op in ops:
145
+ if isinstance(op, dict):
146
+ row = {"className": class_name}
147
+ row.update(op)
148
+ flat.append(row)
149
+ else:
150
+ flat.append({"className": class_name, "value": op})
151
+ else:
152
+ flat.append(item)
153
+ return flat
154
+
155
+
156
+ def _extract_page_info(result: Any) -> dict[str, Any] | None:
157
+ if isinstance(result, dict):
158
+ pi = result.get("pageInfo")
159
+ if isinstance(pi, dict):
160
+ return pi
161
+ payload = result.get("data")
162
+ if isinstance(payload, dict):
163
+ return payload.get("pageInfo")
164
+ return None
165
+
166
+
167
+ def _print_table(
168
+ items: list[dict[str, Any]],
169
+ title: str,
170
+ columns: list[str] | None = None,
171
+ page_info: dict[str, Any] | None = None,
172
+ ) -> None:
173
+ console = get_console()
174
+ cols = columns or list(items[0].keys()) if items else []
175
+ table = Table(title=title, show_header=True, header_style="bold cyan")
176
+ table.add_column("#", style="dim", width=4)
177
+ for col in cols:
178
+ table.add_column(col)
179
+ for idx, item in enumerate(items, 1):
180
+ row = [str(idx)] + [str(item.get(col, "")) for col in cols]
181
+ table.add_row(*row)
182
+ console.print(table)
183
+ if page_info:
184
+ total = page_info.get("totalCount", "?")
185
+ console.print(
186
+ f"[dim]Page {page_info.get('pageIndex', '?')}, "
187
+ f"Size {page_info.get('pageSize', '?')}, "
188
+ f"Total {total}[/dim]"
189
+ )
190
+ else:
191
+ console.print(f"[dim]Total {len(items)} items[/dim]")
192
+
193
+
194
+ # ── App helpers ────────────────────────────────────────────────────────
195
+
196
+
197
+ def _validate_create_params(parsed: dict[str, Any]) -> None:
198
+ title_val = parsed.get("title", "")
199
+ if isinstance(title_val, str) and len(title_val) > 32:
200
+ raise typer.BadParameter(
201
+ f"--title must not exceed 32 characters (got {len(title_val)})"
202
+ )
203
+ app_name_val = parsed.get("appName", "")
204
+ if isinstance(app_name_val, str):
205
+ if len(app_name_val) > 32:
206
+ raise typer.BadParameter(
207
+ f"--app-name must not exceed 32 characters (got {len(app_name_val)})"
208
+ )
209
+ if app_name_val and not app_name_val[0].isalpha():
210
+ raise typer.BadParameter("--app-name must start with a letter")
211
+ if app_name_val and not re.match(r"^[a-zA-Z0-9]+$", app_name_val):
212
+ raise typer.BadParameter("--app-name must contain only letters and digits")
213
+
214
+
215
+ _IMAGE_FIELDS = frozenset({"logoImg", "icoImg"})
216
+ _ALLOWED_IMAGE_EXTENSIONS = frozenset({".jpg", ".jpeg", ".png"})
217
+ _MAX_IMAGE_SIZE = 2 * 1024 * 1024
218
+
219
+
220
+ def _image_to_base64(path: Path, field: str) -> str:
221
+ ext = path.suffix.lower()
222
+ if ext not in _ALLOWED_IMAGE_EXTENSIONS:
223
+ raise typer.BadParameter(
224
+ f"{field} in extConfig must be JPG/JPEG/PNG format (got {ext})"
225
+ )
226
+ size = path.stat().st_size
227
+ if size > _MAX_IMAGE_SIZE:
228
+ raise typer.BadParameter(
229
+ f"{field} in extConfig must not exceed 2MB (got {size / 1024 / 1024:.1f}MB)"
230
+ )
231
+ data = path.read_bytes()
232
+ b64_str = base64.b64encode(data).decode("ascii")
233
+ mime = "image/jpeg" if ext in (".jpg", ".jpeg") else "image/png"
234
+ return f"data:{mime};base64,{b64_str}"
235
+
236
+
237
+ def _convert_image_file_paths(ext_dict: dict[str, Any]) -> None:
238
+ for field in _IMAGE_FIELDS:
239
+ val = ext_dict.get(field)
240
+ if isinstance(val, str) and not val.startswith("data:"):
241
+ p = Path(val)
242
+ if p.is_file():
243
+ ext_dict[field] = _image_to_base64(p, field)
244
+
245
+
246
+ def _stringify_ext_config(payload: Any) -> None:
247
+ if isinstance(payload, dict):
248
+ for key, val in list(payload.items()):
249
+ if key == "extConfig" and isinstance(val, dict):
250
+ _convert_image_file_paths(val)
251
+ payload[key] = json_mod.dumps(val, ensure_ascii=False)
252
+ else:
253
+ _stringify_ext_config(val)
254
+ elif isinstance(payload, list):
255
+ for item in payload:
256
+ _stringify_ext_config(item)
257
+
258
+
259
+ _OP_STRING_FIELDS = frozenset({"oprScript", "extSettings", "viewType"})
260
+
261
+
262
+ def _stringify_op_fields(payload: dict[str, Any]) -> None:
263
+ for field in _OP_STRING_FIELDS:
264
+ val = payload.get(field)
265
+ if isinstance(val, dict):
266
+ payload[field] = json_mod.dumps(val, ensure_ascii=False)
267
+
268
+
269
+ def _resolve_payload(data: str | None, data_file: Path | None) -> dict[str, Any]:
270
+ if data_file:
271
+ return json_mod.loads(data_file.read_text(encoding="utf-8"))
272
+ if data:
273
+ return json_mod.loads(data)
274
+ raise typer.BadParameter("Provide --data or --data-file")
275
+
276
+
277
+ def _generate_app_name(title: str, app_type: str = "PC") -> str:
278
+ raw = app_type + "".join(pypinyin.lazy_pinyin(title, style=pypinyin.Style.NORMAL))
279
+ cleaned = re.sub(r"[^a-zA-Z0-9]", "", raw)
280
+ if len(cleaned) > 29:
281
+ cleaned = cleaned[:29]
282
+ suffix = "".join(
283
+ secrets.choice(string_mod.ascii_lowercase + string_mod.digits) for _ in range(3)
284
+ )
285
+ return cleaned + suffix
286
+
287
+
288
+ # ── App Commands ───────────────────────────────────────────────────────
289
+
290
+
291
+ @_app_sub.command(name="list")
292
+ def list_apps(
293
+ ctx: typer.Context,
294
+ published_only: Annotated[
295
+ bool,
296
+ typer.Option("--published-only", help="Only show published apps"),
297
+ ] = False,
298
+ with_modeler_app: Annotated[
299
+ bool,
300
+ typer.Option(
301
+ "--with-modeler-app/--no-with-modeler-app", help="Include modeler app"
302
+ ),
303
+ ] = True,
304
+ with_ext_config: Annotated[
305
+ bool,
306
+ typer.Option(
307
+ "--with-ext-config/--no-with-ext-config", help="Include ext config"
308
+ ),
309
+ ] = True,
310
+ app_type: Annotated[
311
+ str | None,
312
+ typer.Option("--type", "-t", help="App type: PC, Mobile"),
313
+ ] = None,
314
+ fmt: Annotated[
315
+ OutputFormat,
316
+ typer.Option("--format", "-f", help="Output format: table/json"),
317
+ ] = OutputFormat.table,
318
+ ) -> None:
319
+ try:
320
+ client = get_client(ctx)
321
+ result = funcmodel_api.list_apps(
322
+ client,
323
+ published_only=published_only,
324
+ with_modeler_app=with_modeler_app,
325
+ with_ext_config=with_ext_config,
326
+ app_type=app_type,
327
+ )
328
+ items = _extract_items(result)
329
+ if not items:
330
+ if is_tty():
331
+ get_console().print("[dim]No apps found.[/dim]")
332
+ else:
333
+ typer.echo("[]")
334
+ return
335
+ fmt = resolve_format(fmt)
336
+ if fmt == OutputFormat.json:
337
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
338
+ return
339
+ _print_table(items, title="Applications")
340
+ except DWFError as exc:
341
+ handle_error(exc)
342
+
343
+
344
+ @_app_sub.command(name="get")
345
+ def get_app(
346
+ ctx: typer.Context,
347
+ name: Annotated[
348
+ str | None,
349
+ typer.Option("--name", "-n", help="App name (required if --id not given)"),
350
+ ] = None,
351
+ app_id: Annotated[
352
+ str | None,
353
+ typer.Option("--id", help="App ID (required if --name not given)"),
354
+ ] = None,
355
+ export: Annotated[
356
+ Path | None,
357
+ typer.Option(
358
+ "--export",
359
+ help="Export config to file for editing (then use with app update --data-file)",
360
+ ),
361
+ ] = None,
362
+ fmt: Annotated[
363
+ OutputFormat,
364
+ typer.Option("--format", "-f", help="Output format: table/json"),
365
+ ] = OutputFormat.table,
366
+ ) -> None:
367
+ try:
368
+ client = get_client(ctx)
369
+ if name and app_id:
370
+ raise typer.BadParameter("Use either --name OR --id, not both.")
371
+ if name:
372
+ result = funcmodel_api.get_app_by_name(client, name)
373
+ elif app_id:
374
+ result = funcmodel_api.get_app_by_id(client, app_id)
375
+ else:
376
+ raise typer.BadParameter(
377
+ "Provide one of: --name <app_name> or --id <app_id>"
378
+ )
379
+ if export:
380
+ data = result.get("data", result)
381
+ if isinstance(data.get("extConfig"), str):
382
+ try:
383
+ data["extConfig"] = json_mod.loads(data["extConfig"])
384
+ except json_mod.JSONDecodeError:
385
+ pass
386
+ export.write_text(
387
+ json_mod.dumps(data, ensure_ascii=False, indent=2),
388
+ encoding="utf-8",
389
+ )
390
+ get_console().print(f"[green]Config saved to {export}[/green]")
391
+ return
392
+ fmt = resolve_format(fmt)
393
+ if fmt == OutputFormat.json:
394
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
395
+ return
396
+ item = result.get("data", result)
397
+ if isinstance(item, dict):
398
+ _print_table([item], title="Application")
399
+ else:
400
+ typer.echo(str(result))
401
+ except DWFError as exc:
402
+ handle_error(exc)
403
+
404
+
405
+ @_app_sub.command(name="create")
406
+ def create_app(
407
+ ctx: typer.Context,
408
+ title: Annotated[
409
+ str | None,
410
+ typer.Option("--title", "-t", help="App title (Chinese name)"),
411
+ ] = None,
412
+ app_type: Annotated[
413
+ str,
414
+ typer.Option("--type", help="App type: PC or Mobile"),
415
+ ] = "PC",
416
+ app_name: Annotated[
417
+ str | None,
418
+ typer.Option(
419
+ "--app-name", "-n", help="App code name (auto-generated from title for PC)"
420
+ ),
421
+ ] = None,
422
+ data: Annotated[
423
+ str | None,
424
+ typer.Option(
425
+ "--data",
426
+ "-d",
427
+ help="Full app data as JSON string (alternative to individual options)",
428
+ ),
429
+ ] = None,
430
+ data_file: Annotated[
431
+ Path | None,
432
+ typer.Option(
433
+ "--data-file",
434
+ help="JSON file path (alternative to --data)",
435
+ ),
436
+ ] = None,
437
+ dry_run: Annotated[
438
+ bool, typer.Option("--dry-run", help="Preview without executing")
439
+ ] = False,
440
+ fmt: Annotated[
441
+ OutputFormat,
442
+ typer.Option("--format", "-f", help="Output format: table/json"),
443
+ ] = OutputFormat.table,
444
+ ) -> None:
445
+ try:
446
+ if data is not None or data_file is not None:
447
+ raw = _resolve_data(data, data_file)
448
+ try:
449
+ parsed = json_mod.loads(raw)
450
+ except json_mod.JSONDecodeError as e:
451
+ Console().print(f"[red]Invalid JSON: {e}[/red]")
452
+ raise typer.Exit(code=2)
453
+ _stringify_ext_config(parsed)
454
+ _validate_create_params(parsed)
455
+ else:
456
+ if not title:
457
+ raise typer.BadParameter("--title is required when not using --data")
458
+ app_type_normalized = app_type.upper()
459
+ if app_type_normalized not in ("PC", "MOBILE"):
460
+ raise typer.BadParameter("--type must be 'PC' or 'Mobile'")
461
+ app_type = "PC" if app_type_normalized == "PC" else "Mobile"
462
+ if not app_name:
463
+ if app_type == "PC":
464
+ app_name = _generate_app_name(title, app_type)
465
+ else:
466
+ raise typer.BadParameter(
467
+ "--app-name is required when --type is Mobile"
468
+ )
469
+ parsed: dict[str, Any] = {
470
+ "title": title,
471
+ "type": app_type,
472
+ "appName": app_name,
473
+ }
474
+ _validate_create_params(parsed)
475
+ if dry_run:
476
+ typer.echo(
477
+ json_mod.dumps(
478
+ {
479
+ "action": "create",
480
+ "resource": "app",
481
+ "target": parsed,
482
+ "reversible": True,
483
+ },
484
+ ensure_ascii=False,
485
+ indent=2,
486
+ )
487
+ )
488
+ raise typer.Exit(code=10)
489
+ client = get_client(ctx)
490
+ result = funcmodel_api.create_app(client, parsed)
491
+ raise_if_not_success(result, "Create app")
492
+ detail = parsed.get("appName") or parsed.get("title")
493
+ report_success(SuccessResult("Created", "app", detail=detail, data=result), fmt)
494
+ except DWFError as exc:
495
+ handle_error(exc)
496
+
497
+
498
+ @_app_sub.command(name="update")
499
+ def update_app(
500
+ ctx: typer.Context,
501
+ title: Annotated[
502
+ str | None,
503
+ typer.Option("--title", "-t", help="App title (Chinese name)"),
504
+ ] = None,
505
+ app_name: Annotated[
506
+ str | None,
507
+ typer.Option("--app-name", "-n", help="App code name"),
508
+ ] = None,
509
+ data: Annotated[
510
+ str | None,
511
+ typer.Option(
512
+ "--data",
513
+ "-d",
514
+ help="Full app data as JSON string (alternative to individual options)",
515
+ ),
516
+ ] = None,
517
+ data_file: Annotated[
518
+ Path | None,
519
+ typer.Option(
520
+ "--data-file",
521
+ help="JSON file path (alternative to --data)",
522
+ ),
523
+ ] = None,
524
+ dry_run: Annotated[
525
+ bool, typer.Option("--dry-run", help="Preview without executing")
526
+ ] = False,
527
+ fmt: Annotated[
528
+ OutputFormat,
529
+ typer.Option("--format", "-f", help="Output format: table/json"),
530
+ ] = OutputFormat.table,
531
+ ) -> None:
532
+ try:
533
+ if data is not None or data_file is not None:
534
+ raw = _resolve_data(data, data_file)
535
+ try:
536
+ parsed = json_mod.loads(raw)
537
+ except json_mod.JSONDecodeError as e:
538
+ Console().print(f"[red]Invalid JSON: {e}[/red]")
539
+ raise typer.Exit(code=2)
540
+ _stringify_ext_config(parsed)
541
+ _validate_create_params(parsed)
542
+ else:
543
+ parsed: dict[str, Any] = {}
544
+ if title:
545
+ parsed["title"] = title
546
+ if app_name:
547
+ parsed["appName"] = app_name
548
+ if not parsed:
549
+ raise typer.BadParameter(
550
+ "Provide at least one of: --title, --app-name, or use --data/--data-file"
551
+ )
552
+ _validate_create_params(parsed)
553
+ if dry_run:
554
+ typer.echo(
555
+ json_mod.dumps(
556
+ {
557
+ "action": "update",
558
+ "resource": "app",
559
+ "target": parsed,
560
+ "reversible": True,
561
+ },
562
+ ensure_ascii=False,
563
+ indent=2,
564
+ )
565
+ )
566
+ raise typer.Exit(code=10)
567
+ client = get_client(ctx)
568
+ result = funcmodel_api.update_app(client, parsed)
569
+ raise_if_not_success(result, "Update app")
570
+ detail = parsed.get("appName")
571
+ report_success(SuccessResult("Updated", "app", detail=detail, data=result), fmt)
572
+ except DWFError as exc:
573
+ handle_error(exc)
574
+
575
+
576
+ @_app_sub.command(name="delete")
577
+ def delete_app(
578
+ ctx: typer.Context,
579
+ app_id: Annotated[str, typer.Argument(help="App ID to delete")],
580
+ yes: Annotated[
581
+ bool,
582
+ typer.Option("--yes", "-y", help="Confirm deletion"),
583
+ ] = False,
584
+ dry_run: Annotated[
585
+ bool, typer.Option("--dry-run", help="Preview without executing")
586
+ ] = False,
587
+ fmt: Annotated[
588
+ OutputFormat,
589
+ typer.Option("--format", "-f", help="Output format: table/json"),
590
+ ] = OutputFormat.table,
591
+ ) -> None:
592
+ try:
593
+ if dry_run:
594
+ typer.echo(
595
+ json_mod.dumps(
596
+ {
597
+ "action": "delete",
598
+ "resource": "app",
599
+ "target": {"app_id": app_id},
600
+ "reversible": False,
601
+ },
602
+ ensure_ascii=False,
603
+ indent=2,
604
+ )
605
+ )
606
+ raise typer.Exit(code=10)
607
+ if not yes and is_tty():
608
+ confirmed = typer.confirm(f"Delete app '{app_id}'?")
609
+ if not confirmed:
610
+ raise typer.Exit(code=0)
611
+ client = get_client(ctx)
612
+ result = funcmodel_api.delete_app(client, app_id)
613
+ raise_if_not_success(result, "Delete app")
614
+ report_success(SuccessResult("Deleted", "app", detail=app_id, data=result), fmt)
615
+ except DWFError as exc:
616
+ handle_error(exc)
617
+
618
+
619
+ @_app_sub.command(name="set-default")
620
+ def set_default_app(
621
+ ctx: typer.Context,
622
+ app_id: Annotated[str, typer.Argument(help="App ID")],
623
+ set_default: Annotated[
624
+ bool,
625
+ typer.Option("--set-default/--unset-default", help="Set or unset as default"),
626
+ ] = True,
627
+ dry_run: Annotated[
628
+ bool, typer.Option("--dry-run", help="Preview without executing")
629
+ ] = False,
630
+ fmt: Annotated[
631
+ OutputFormat,
632
+ typer.Option("--format", "-f", help="Output format: table/json"),
633
+ ] = OutputFormat.table,
634
+ ) -> None:
635
+ try:
636
+ if dry_run:
637
+ typer.echo(
638
+ json_mod.dumps(
639
+ {
640
+ "action": "set_default",
641
+ "resource": "app",
642
+ "target": {"app_id": app_id, "set_default": set_default},
643
+ "reversible": True,
644
+ },
645
+ ensure_ascii=False,
646
+ indent=2,
647
+ )
648
+ )
649
+ raise typer.Exit(code=10)
650
+ client = get_client(ctx)
651
+ result = funcmodel_api.set_default_app(client, app_id, set_default)
652
+ raise_if_not_success(result, "Set default app")
653
+ action_label = "Set as default" if set_default else "Unset as default"
654
+ report_success(
655
+ SuccessResult(action_label, "app", detail=app_id, data=result), fmt
656
+ )
657
+ except DWFError as exc:
658
+ handle_error(exc)
659
+
660
+
661
+ @_app_sub.command(name="publish")
662
+ def publish_app(
663
+ ctx: typer.Context,
664
+ app_id: Annotated[str, typer.Argument(help="App ID")],
665
+ publish: Annotated[
666
+ bool,
667
+ typer.Option("--publish/--unpublish", help="Publish or unpublish"),
668
+ ] = True,
669
+ dry_run: Annotated[
670
+ bool, typer.Option("--dry-run", help="Preview without executing")
671
+ ] = False,
672
+ fmt: Annotated[
673
+ OutputFormat,
674
+ typer.Option("--format", "-f", help="Output format: table/json"),
675
+ ] = OutputFormat.table,
676
+ ) -> None:
677
+ try:
678
+ if dry_run:
679
+ typer.echo(
680
+ json_mod.dumps(
681
+ {
682
+ "action": "publish" if publish else "unpublish",
683
+ "resource": "app",
684
+ "target": {"app_id": app_id},
685
+ "reversible": True,
686
+ },
687
+ ensure_ascii=False,
688
+ indent=2,
689
+ )
690
+ )
691
+ raise typer.Exit(code=10)
692
+ client = get_client(ctx)
693
+ result = funcmodel_api.publish_app(client, app_id, publish)
694
+ raise_if_not_success(result, "Publish app")
695
+ action_label = "Published" if publish else "Unpublished"
696
+ report_success(
697
+ SuccessResult(action_label, "app", detail=app_id, data=result), fmt
698
+ )
699
+ except DWFError as exc:
700
+ handle_error(exc)
701
+
702
+
703
+ @_app_sub.command(name="get-default")
704
+ def get_default_app(
705
+ ctx: typer.Context,
706
+ app_type: Annotated[str, typer.Argument(help="App type: PC or Mobile")],
707
+ fmt: Annotated[
708
+ OutputFormat,
709
+ typer.Option("--format", "-f", help="Output format: table/json"),
710
+ ] = OutputFormat.table,
711
+ ) -> None:
712
+ try:
713
+ client = get_client(ctx)
714
+ result = funcmodel_api.get_default_app_by_type(client, app_type)
715
+ fmt = resolve_format(fmt)
716
+ if fmt == OutputFormat.json:
717
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
718
+ return
719
+ item = result.get("data", result)
720
+ if isinstance(item, dict):
721
+ _print_table([item], title=f"Default {app_type} App")
722
+ else:
723
+ typer.echo(str(result))
724
+ except DWFError as exc:
725
+ handle_error(exc)
726
+
727
+
728
+ @_app_sub.command(name="reset-modeler")
729
+ def reset_modeler_menus(
730
+ ctx: typer.Context,
731
+ yes: Annotated[
732
+ bool,
733
+ typer.Option("--yes", "-y", help="Confirm reset"),
734
+ ] = False,
735
+ dry_run: Annotated[
736
+ bool, typer.Option("--dry-run", help="Preview without executing")
737
+ ] = False,
738
+ fmt: Annotated[
739
+ OutputFormat,
740
+ typer.Option("--format", "-f", help="Output format: table/json"),
741
+ ] = OutputFormat.table,
742
+ ) -> None:
743
+ try:
744
+ if dry_run:
745
+ typer.echo(
746
+ json_mod.dumps(
747
+ {
748
+ "action": "reset",
749
+ "resource": "modeler_menus",
750
+ "reversible": False,
751
+ },
752
+ ensure_ascii=False,
753
+ indent=2,
754
+ )
755
+ )
756
+ raise typer.Exit(code=10)
757
+ if not yes and is_tty():
758
+ confirmed = typer.confirm("Reset all modeler menus?")
759
+ if not confirmed:
760
+ raise typer.Exit(code=0)
761
+ client = get_client(ctx)
762
+ result = funcmodel_api.reset_modeler_menus(client)
763
+ raise_if_not_success(result, "Reset modeler menus")
764
+ report_success(SuccessResult("Reset", "modeler menus", data=result), fmt)
765
+ except DWFError as exc:
766
+ handle_error(exc)
767
+
768
+
769
+ @_app_sub.command(name="upload-logo")
770
+ def upload_logo(
771
+ ctx: typer.Context,
772
+ app_id: Annotated[str, typer.Argument(help="App ID")],
773
+ file: Annotated[Path, typer.Option("--file", "-f", help="Logo file path")],
774
+ dry_run: Annotated[
775
+ bool, typer.Option("--dry-run", help="Preview without executing")
776
+ ] = False,
777
+ fmt: Annotated[
778
+ OutputFormat,
779
+ typer.Option("--format", "-f", help="Output format: table/json"),
780
+ ] = OutputFormat.table,
781
+ ) -> None:
782
+ try:
783
+ if dry_run:
784
+ typer.echo(
785
+ json_mod.dumps(
786
+ {
787
+ "action": "upload_logo",
788
+ "resource": "app",
789
+ "target": {"app_id": app_id, "file": str(file)},
790
+ },
791
+ ensure_ascii=False,
792
+ indent=2,
793
+ )
794
+ )
795
+ raise typer.Exit(code=10)
796
+ client = get_client(ctx)
797
+ result = funcmodel_api.upload_logo(client, app_id, file)
798
+ raise_if_not_success(result, "Upload logo")
799
+ report_success(
800
+ SuccessResult("Uploaded", "logo", detail=app_id, data=result), fmt
801
+ )
802
+ except DWFError as exc:
803
+ handle_error(exc)
804
+
805
+
806
+ @_app_sub.command(name="upload-icon")
807
+ def upload_icon(
808
+ ctx: typer.Context,
809
+ app_id: Annotated[str, typer.Argument(help="App ID")],
810
+ file: Annotated[Path, typer.Option("--file", "-f", help="Icon file path")],
811
+ dry_run: Annotated[
812
+ bool, typer.Option("--dry-run", help="Preview without executing")
813
+ ] = False,
814
+ fmt: Annotated[
815
+ OutputFormat,
816
+ typer.Option("--format", "-f", help="Output format: table/json"),
817
+ ] = OutputFormat.table,
818
+ ) -> None:
819
+ try:
820
+ if dry_run:
821
+ typer.echo(
822
+ json_mod.dumps(
823
+ {
824
+ "action": "upload_icon",
825
+ "resource": "app",
826
+ "target": {"app_id": app_id, "file": str(file)},
827
+ },
828
+ ensure_ascii=False,
829
+ indent=2,
830
+ )
831
+ )
832
+ raise typer.Exit(code=10)
833
+ client = get_client(ctx)
834
+ result = funcmodel_api.upload_icon(client, app_id, file)
835
+ raise_if_not_success(result, "Upload icon")
836
+ report_success(
837
+ SuccessResult("Uploaded", "icon", detail=app_id, data=result), fmt
838
+ )
839
+ except DWFError as exc:
840
+ handle_error(exc)
841
+
842
+
843
+ @_app_sub.command(name="get-logo")
844
+ def get_logo(
845
+ ctx: typer.Context,
846
+ app_id: Annotated[str, typer.Argument(help="App ID")],
847
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")],
848
+ ) -> None:
849
+ try:
850
+ client = get_client(ctx)
851
+ response = funcmodel_api.get_logo(client, app_id)
852
+ with open(output, "wb") as f:
853
+ f.write(response.content)
854
+ Console().print(f"[green]Logo saved to {output}[/green]")
855
+ except DWFError as exc:
856
+ handle_error(exc)
857
+
858
+
859
+ @_app_sub.command(name="get-logo-base64")
860
+ def get_logo_base64(
861
+ ctx: typer.Context,
862
+ app_id: Annotated[str, typer.Argument(help="App ID")],
863
+ img_type: Annotated[
864
+ str,
865
+ typer.Option("--img-type", help="Image keyword: logoImg, iconImg"),
866
+ ] = "logoImg",
867
+ fmt: Annotated[
868
+ OutputFormat,
869
+ typer.Option("--format", "-f", help="Output format: table/json"),
870
+ ] = OutputFormat.table,
871
+ ) -> None:
872
+ try:
873
+ client = get_client(ctx)
874
+ result = funcmodel_api.get_logo_base64(client, app_id, img_type=img_type)
875
+ fmt = resolve_format(fmt)
876
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
877
+ except DWFError as exc:
878
+ handle_error(exc)
879
+
880
+
881
+ @_app_sub.command(name="get-css")
882
+ def get_css(
883
+ ctx: typer.Context,
884
+ app_id: Annotated[str, typer.Argument(help="App ID")],
885
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")],
886
+ ) -> None:
887
+ try:
888
+ client = get_client(ctx)
889
+ response = funcmodel_api.get_css_file(client, app_id)
890
+ with open(output, "wb") as f:
891
+ f.write(response.content)
892
+ Console().print(f"[green]CSS file saved to {output}[/green]")
893
+ except DWFError as exc:
894
+ handle_error(exc)
895
+
896
+
897
+ # ── Theme Commands ─────────────────────────────────────────────────────
898
+
899
+
900
+ @_theme_sub.command(name="list")
901
+ def list_themes(
902
+ ctx: typer.Context,
903
+ fmt: Annotated[
904
+ OutputFormat,
905
+ typer.Option("--format", "-f", help="Output format: table/json"),
906
+ ] = OutputFormat.table,
907
+ ) -> None:
908
+ try:
909
+ client = get_client(ctx)
910
+ result = funcmodel_api.list_themes(client)
911
+ items = _extract_items(result)
912
+ if not items:
913
+ if is_tty():
914
+ Console().print("[dim]No themes found.[/dim]")
915
+ else:
916
+ typer.echo("[]")
917
+ return
918
+ fmt = resolve_format(fmt)
919
+ if fmt == OutputFormat.json:
920
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
921
+ return
922
+ _print_table(items, title="Themes")
923
+ except DWFError as exc:
924
+ handle_error(exc)
925
+
926
+
927
+ @_theme_sub.command(name="create")
928
+ def create_theme(
929
+ ctx: typer.Context,
930
+ theme_name: Annotated[str, typer.Option("--name", "-n", help="Theme name")],
931
+ file: Annotated[Path, typer.Option("--file", help="Theme CSS file path")],
932
+ note: Annotated[str | None, typer.Option("--note", help="Theme note")] = None,
933
+ dry_run: Annotated[
934
+ bool, typer.Option("--dry-run", help="Preview without executing")
935
+ ] = False,
936
+ fmt: Annotated[
937
+ OutputFormat,
938
+ typer.Option("--format", "-f", help="Output format: table/json"),
939
+ ] = OutputFormat.table,
940
+ ) -> None:
941
+ try:
942
+ if dry_run:
943
+ typer.echo(
944
+ json_mod.dumps(
945
+ {
946
+ "action": "create",
947
+ "resource": "theme",
948
+ "target": {"theme_name": theme_name, "note": note},
949
+ "reversible": True,
950
+ },
951
+ ensure_ascii=False,
952
+ indent=2,
953
+ )
954
+ )
955
+ raise typer.Exit(code=10)
956
+ client = get_client(ctx)
957
+ result = funcmodel_api.create_theme(
958
+ client, theme_name=theme_name, note=note, file_path=file
959
+ )
960
+ raise_if_not_success(result, "Create theme")
961
+ report_success(
962
+ SuccessResult("Created", "theme", detail=theme_name, data=result), fmt
963
+ )
964
+ except DWFError as exc:
965
+ handle_error(exc)
966
+
967
+
968
+ @_theme_sub.command(name="update")
969
+ def update_theme(
970
+ ctx: typer.Context,
971
+ oid: Annotated[str, typer.Argument(help="Theme OID")],
972
+ theme_name: Annotated[
973
+ str | None, typer.Option("--name", "-n", help="Theme name")
974
+ ] = None,
975
+ note: Annotated[str | None, typer.Option("--note", help="Theme note")] = None,
976
+ file: Annotated[
977
+ Path | None, typer.Option("--file", "-f", help="Theme CSS file path")
978
+ ] = None,
979
+ dry_run: Annotated[
980
+ bool, typer.Option("--dry-run", help="Preview without executing")
981
+ ] = False,
982
+ fmt: Annotated[
983
+ OutputFormat,
984
+ typer.Option("--format", "-f", help="Output format: table/json"),
985
+ ] = OutputFormat.table,
986
+ ) -> None:
987
+ try:
988
+ if dry_run:
989
+ typer.echo(
990
+ json_mod.dumps(
991
+ {
992
+ "action": "update",
993
+ "resource": "theme",
994
+ "target": {"oid": oid},
995
+ "reversible": True,
996
+ },
997
+ ensure_ascii=False,
998
+ indent=2,
999
+ )
1000
+ )
1001
+ raise typer.Exit(code=10)
1002
+ client = get_client(ctx)
1003
+ result = funcmodel_api.update_theme(
1004
+ client, oid, theme_name=theme_name, note=note, file_path=file
1005
+ )
1006
+ raise_if_not_success(result, "Update theme")
1007
+ report_success(SuccessResult("Updated", "theme", detail=oid, data=result), fmt)
1008
+ except DWFError as exc:
1009
+ handle_error(exc)
1010
+
1011
+
1012
+ @_theme_sub.command(name="delete")
1013
+ def delete_theme(
1014
+ ctx: typer.Context,
1015
+ oid: Annotated[str, typer.Argument(help="Theme OID")],
1016
+ yes: Annotated[
1017
+ bool,
1018
+ typer.Option("--yes", "-y", help="Confirm deletion"),
1019
+ ] = False,
1020
+ dry_run: Annotated[
1021
+ bool, typer.Option("--dry-run", help="Preview without executing")
1022
+ ] = False,
1023
+ fmt: Annotated[
1024
+ OutputFormat,
1025
+ typer.Option("--format", "-f", help="Output format: table/json"),
1026
+ ] = OutputFormat.table,
1027
+ ) -> None:
1028
+ try:
1029
+ if dry_run:
1030
+ typer.echo(
1031
+ json_mod.dumps(
1032
+ {
1033
+ "action": "delete",
1034
+ "resource": "theme",
1035
+ "target": {"oid": oid},
1036
+ "reversible": False,
1037
+ },
1038
+ ensure_ascii=False,
1039
+ indent=2,
1040
+ )
1041
+ )
1042
+ raise typer.Exit(code=10)
1043
+ if not yes and is_tty():
1044
+ confirmed = typer.confirm(f"Delete theme '{oid}'?")
1045
+ if not confirmed:
1046
+ raise typer.Exit(code=0)
1047
+ client = get_client(ctx)
1048
+ result = funcmodel_api.delete_theme(client, oid)
1049
+ raise_if_not_success(result, "Delete theme")
1050
+ report_success(SuccessResult("Deleted", "theme", detail=oid, data=result), fmt)
1051
+ except DWFError as exc:
1052
+ handle_error(exc)
1053
+
1054
+
1055
+ # ── Language Package Commands ──────────────────────────────────────────
1056
+
1057
+
1058
+ @_lang_pkg_sub.command(name="list")
1059
+ def list_language_packages(
1060
+ ctx: typer.Context,
1061
+ app_id: Annotated[str, typer.Option("--app-id", help="App ID")],
1062
+ fmt: Annotated[
1063
+ OutputFormat,
1064
+ typer.Option("--format", "-f", help="Output format: table/json"),
1065
+ ] = OutputFormat.table,
1066
+ ) -> None:
1067
+ try:
1068
+ client = get_client(ctx)
1069
+ result = funcmodel_api.list_language_packages(client, app_id)
1070
+ items = _extract_items(result)
1071
+ if not items:
1072
+ if is_tty():
1073
+ Console().print("[dim]No language packages found.[/dim]")
1074
+ else:
1075
+ typer.echo("[]")
1076
+ return
1077
+ fmt = resolve_format(fmt)
1078
+ if fmt == OutputFormat.json:
1079
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1080
+ return
1081
+ _print_table(items, title=f"Language Packages (app: {app_id})")
1082
+ except DWFError as exc:
1083
+ handle_error(exc)
1084
+
1085
+
1086
+ @_lang_pkg_sub.command(name="get")
1087
+ def get_language_package(
1088
+ ctx: typer.Context,
1089
+ app_id: Annotated[str, typer.Option("--app-id", help="App ID")],
1090
+ language: Annotated[str, typer.Option("--language", "-l", help="Language code")],
1091
+ fmt: Annotated[
1092
+ OutputFormat,
1093
+ typer.Option("--format", "-f", help="Output format: table/json"),
1094
+ ] = OutputFormat.table,
1095
+ ) -> None:
1096
+ try:
1097
+ client = get_client(ctx)
1098
+ result = funcmodel_api.get_language_package_by_language(
1099
+ client, app_id, language
1100
+ )
1101
+ fmt = resolve_format(fmt)
1102
+ if fmt == OutputFormat.json:
1103
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1104
+ return
1105
+ item = result.get("data", result)
1106
+ if isinstance(item, dict):
1107
+ _print_table([item], title=f"Language Package ({language})")
1108
+ else:
1109
+ typer.echo(str(result))
1110
+ except DWFError as exc:
1111
+ handle_error(exc)
1112
+
1113
+
1114
+ @_lang_pkg_sub.command(name="get-json")
1115
+ def get_language_package_json(
1116
+ ctx: typer.Context,
1117
+ app_id: Annotated[str, typer.Option("--app-id", help="App ID")],
1118
+ language: Annotated[str, typer.Option("--language", "-l", help="Language code")],
1119
+ is_frontend: Annotated[
1120
+ bool,
1121
+ typer.Option("--is-frontend/--not-frontend", help="Is frontend config file"),
1122
+ ] = True,
1123
+ fmt: Annotated[
1124
+ OutputFormat,
1125
+ typer.Option("--format", "-f", help="Output format: table/json"),
1126
+ ] = OutputFormat.table,
1127
+ ) -> None:
1128
+ try:
1129
+ client = get_client(ctx)
1130
+ result = funcmodel_api.get_language_package_json(
1131
+ client, app_id, language, is_frontend=is_frontend
1132
+ )
1133
+ fmt = resolve_format(fmt)
1134
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1135
+ except DWFError as exc:
1136
+ handle_error(exc)
1137
+
1138
+
1139
+ @_lang_pkg_sub.command(name="get-file")
1140
+ def get_language_package_file(
1141
+ ctx: typer.Context,
1142
+ app_id: Annotated[str, typer.Option("--app-id", help="App ID")],
1143
+ language: Annotated[str, typer.Option("--language", "-l", help="Language code")],
1144
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")],
1145
+ ) -> None:
1146
+ try:
1147
+ client = get_client(ctx)
1148
+ response = funcmodel_api.get_language_package_file(client, app_id, language)
1149
+ with open(output, "wb") as f:
1150
+ f.write(response.content)
1151
+ Console().print(f"[green]File saved to {output}[/green]")
1152
+ except DWFError as exc:
1153
+ handle_error(exc)
1154
+
1155
+
1156
+ @_lang_pkg_sub.command(name="create")
1157
+ def create_language_package(
1158
+ ctx: typer.Context,
1159
+ app_language_package_do: Annotated[
1160
+ str,
1161
+ typer.Option("--data", "-d", help="Language package DO JSON string"),
1162
+ ],
1163
+ file: Annotated[Path, typer.Option("--file", "-f", help="Language package file")],
1164
+ dry_run: Annotated[
1165
+ bool, typer.Option("--dry-run", help="Preview without executing")
1166
+ ] = False,
1167
+ fmt: Annotated[
1168
+ OutputFormat,
1169
+ typer.Option("--format", "-f", help="Output format: table/json"),
1170
+ ] = OutputFormat.table,
1171
+ ) -> None:
1172
+ try:
1173
+ if dry_run:
1174
+ typer.echo(
1175
+ json_mod.dumps(
1176
+ {
1177
+ "action": "create",
1178
+ "resource": "language_package",
1179
+ "target": {"data": app_language_package_do, "file": str(file)},
1180
+ "reversible": True,
1181
+ },
1182
+ ensure_ascii=False,
1183
+ indent=2,
1184
+ )
1185
+ )
1186
+ raise typer.Exit(code=10)
1187
+ client = get_client(ctx)
1188
+ result = funcmodel_api.create_language_package(
1189
+ client, app_language_package_do, file
1190
+ )
1191
+ raise_if_not_success(result, "Create language package")
1192
+ report_success(SuccessResult("Created", "language package", data=result), fmt)
1193
+ except DWFError as exc:
1194
+ handle_error(exc)
1195
+
1196
+
1197
+ @_lang_pkg_sub.command(name="update")
1198
+ def update_language_package(
1199
+ ctx: typer.Context,
1200
+ package_id: Annotated[str, typer.Argument(help="Language package ID")],
1201
+ file: Annotated[Path, typer.Option("--file", "-f", help="Language package file")],
1202
+ dry_run: Annotated[
1203
+ bool, typer.Option("--dry-run", help="Preview without executing")
1204
+ ] = False,
1205
+ fmt: Annotated[
1206
+ OutputFormat,
1207
+ typer.Option("--format", "-f", help="Output format: table/json"),
1208
+ ] = OutputFormat.table,
1209
+ ) -> None:
1210
+ try:
1211
+ if dry_run:
1212
+ typer.echo(
1213
+ json_mod.dumps(
1214
+ {
1215
+ "action": "update",
1216
+ "resource": "language_package",
1217
+ "target": {"package_id": package_id, "file": str(file)},
1218
+ "reversible": True,
1219
+ },
1220
+ ensure_ascii=False,
1221
+ indent=2,
1222
+ )
1223
+ )
1224
+ raise typer.Exit(code=10)
1225
+ client = get_client(ctx)
1226
+ result = funcmodel_api.update_language_package(client, package_id, file)
1227
+ raise_if_not_success(result, "Update language package")
1228
+ report_success(
1229
+ SuccessResult(
1230
+ "Updated", "language package", detail=package_id, data=result
1231
+ ),
1232
+ fmt,
1233
+ )
1234
+ except DWFError as exc:
1235
+ handle_error(exc)
1236
+
1237
+
1238
+ @_lang_pkg_sub.command(name="delete")
1239
+ def delete_language_package(
1240
+ ctx: typer.Context,
1241
+ package_id: Annotated[str, typer.Argument(help="Language package ID")],
1242
+ yes: Annotated[
1243
+ bool,
1244
+ typer.Option("--yes", "-y", help="Confirm deletion"),
1245
+ ] = False,
1246
+ dry_run: Annotated[
1247
+ bool, typer.Option("--dry-run", help="Preview without executing")
1248
+ ] = False,
1249
+ fmt: Annotated[
1250
+ OutputFormat,
1251
+ typer.Option("--format", "-f", help="Output format: table/json"),
1252
+ ] = OutputFormat.table,
1253
+ ) -> None:
1254
+ try:
1255
+ if dry_run:
1256
+ typer.echo(
1257
+ json_mod.dumps(
1258
+ {
1259
+ "action": "delete",
1260
+ "resource": "language_package",
1261
+ "target": {"package_id": package_id},
1262
+ "reversible": False,
1263
+ },
1264
+ ensure_ascii=False,
1265
+ indent=2,
1266
+ )
1267
+ )
1268
+ raise typer.Exit(code=10)
1269
+ if not yes and is_tty():
1270
+ confirmed = typer.confirm(f"Delete language package '{package_id}'?")
1271
+ if not confirmed:
1272
+ raise typer.Exit(code=0)
1273
+ client = get_client(ctx)
1274
+ result = funcmodel_api.delete_language_package(client, package_id)
1275
+ raise_if_not_success(result, "Delete language package")
1276
+ report_success(
1277
+ SuccessResult(
1278
+ "Deleted", "language package", detail=package_id, data=result
1279
+ ),
1280
+ fmt,
1281
+ )
1282
+ except DWFError as exc:
1283
+ handle_error(exc)
1284
+
1285
+
1286
+ @_lang_pkg_sub.command(name="delete-by-app")
1287
+ def delete_language_packages_by_app(
1288
+ ctx: typer.Context,
1289
+ app_id: Annotated[str, typer.Argument(help="App ID")],
1290
+ yes: Annotated[
1291
+ bool,
1292
+ typer.Option("--yes", "-y", help="Confirm deletion"),
1293
+ ] = False,
1294
+ dry_run: Annotated[
1295
+ bool, typer.Option("--dry-run", help="Preview without executing")
1296
+ ] = False,
1297
+ fmt: Annotated[
1298
+ OutputFormat,
1299
+ typer.Option("--format", "-f", help="Output format: table/json"),
1300
+ ] = OutputFormat.table,
1301
+ ) -> None:
1302
+ try:
1303
+ if dry_run:
1304
+ typer.echo(
1305
+ json_mod.dumps(
1306
+ {
1307
+ "action": "delete_by_app",
1308
+ "resource": "language_package",
1309
+ "target": {"app_id": app_id},
1310
+ "reversible": False,
1311
+ },
1312
+ ensure_ascii=False,
1313
+ indent=2,
1314
+ )
1315
+ )
1316
+ raise typer.Exit(code=10)
1317
+ if not yes and is_tty():
1318
+ confirmed = typer.confirm(
1319
+ f"Delete all language packages for app '{app_id}'?"
1320
+ )
1321
+ if not confirmed:
1322
+ raise typer.Exit(code=0)
1323
+ client = get_client(ctx)
1324
+ result = funcmodel_api.delete_language_packages_by_app(client, app_id)
1325
+ raise_if_not_success(result, "Delete language packages by app")
1326
+ report_success(
1327
+ SuccessResult(
1328
+ "Deleted", "language packages", detail=f"app '{app_id}'", data=result
1329
+ ),
1330
+ fmt,
1331
+ )
1332
+ except DWFError as exc:
1333
+ handle_error(exc)
1334
+
1335
+
1336
+ @_lang_pkg_sub.command(name="types")
1337
+ def list_supported_language_types(
1338
+ ctx: typer.Context,
1339
+ mobile_only: Annotated[
1340
+ bool,
1341
+ typer.Option(
1342
+ "--mobile-only/--no-mobile-only", help="Only mobile supported types"
1343
+ ),
1344
+ ] = False,
1345
+ fmt: Annotated[
1346
+ OutputFormat,
1347
+ typer.Option("--format", "-f", help="Output format: table/json"),
1348
+ ] = OutputFormat.table,
1349
+ ) -> None:
1350
+ try:
1351
+ client = get_client(ctx)
1352
+ result = funcmodel_api.get_supported_language_types(
1353
+ client, mobile_only=mobile_only
1354
+ )
1355
+ fmt = resolve_format(fmt)
1356
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1357
+ except DWFError as exc:
1358
+ handle_error(exc)
1359
+
1360
+
1361
+ @_lang_pkg_sub.command(name="get-template")
1362
+ def get_language_package_template(
1363
+ ctx: typer.Context,
1364
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")],
1365
+ template_type: Annotated[
1366
+ str,
1367
+ typer.Option("--type", "-t", help="Template type: PC, Mobile"),
1368
+ ] = "PC",
1369
+ ) -> None:
1370
+ try:
1371
+ client = get_client(ctx)
1372
+ response = funcmodel_api.get_language_package_template_file(
1373
+ client, template_type
1374
+ )
1375
+ with open(output, "wb") as f:
1376
+ f.write(response.content)
1377
+ Console().print(f"[green]Template saved to {output}[/green]")
1378
+ except DWFError as exc:
1379
+ handle_error(exc)
1380
+
1381
+
1382
+ @_lang_pkg_sub.command(name="get-template-json")
1383
+ def get_language_package_template_json(
1384
+ ctx: typer.Context,
1385
+ template_type: Annotated[
1386
+ str,
1387
+ typer.Option("--type", "-t", help="Template type: PC, Mobile"),
1388
+ ] = "PC",
1389
+ second_key: Annotated[
1390
+ str | None,
1391
+ typer.Option("--second-key", help="Second level key under zh_CN"),
1392
+ ] = None,
1393
+ fmt: Annotated[
1394
+ OutputFormat,
1395
+ typer.Option("--format", "-f", help="Output format: table/json"),
1396
+ ] = OutputFormat.table,
1397
+ ) -> None:
1398
+ try:
1399
+ client = get_client(ctx)
1400
+ result = funcmodel_api.get_language_package_template_json(
1401
+ client, template_type=template_type, second_key=second_key
1402
+ )
1403
+ fmt = resolve_format(fmt)
1404
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1405
+ except DWFError as exc:
1406
+ handle_error(exc)
1407
+
1408
+
1409
+ # ── Operations Commands ────────────────────────────────────────────────
1410
+
1411
+
1412
+ @_operations_sub.command(name="list")
1413
+ def list_operations(
1414
+ ctx: typer.Context,
1415
+ operation_type: Annotated[
1416
+ OperationType,
1417
+ typer.Option(
1418
+ "--type",
1419
+ "-t",
1420
+ help="Operation type: global, entity, relation",
1421
+ case_sensitive=False,
1422
+ ),
1423
+ ] = OperationType.global_,
1424
+ page: Annotated[int, typer.Option("--page", "-p", help="Page index (0-based)")] = 0,
1425
+ page_size: Annotated[
1426
+ int, typer.Option("--page-size", "-s", help="Items per page")
1427
+ ] = 25,
1428
+ page_all: Annotated[
1429
+ bool,
1430
+ typer.Option("--page-all", help="Fetch all pages automatically"),
1431
+ ] = False,
1432
+ fuzzy_search: Annotated[
1433
+ str | None,
1434
+ typer.Option("--search", help="Fuzzy search keyword"),
1435
+ ] = None,
1436
+ fmt: Annotated[
1437
+ OutputFormat,
1438
+ typer.Option("--format", "-f", help="Output format: table/json"),
1439
+ ] = OutputFormat.table,
1440
+ ) -> None:
1441
+ try:
1442
+ client = get_client(ctx)
1443
+ if page_all:
1444
+ all_items: list[dict[str, Any]] = []
1445
+ page_idx = 0
1446
+ while True:
1447
+ r = funcmodel_api.list_operations(
1448
+ client,
1449
+ operation_type.value,
1450
+ page=page_idx,
1451
+ page_size=page_size,
1452
+ fuzzy_search=fuzzy_search,
1453
+ )
1454
+ p_items = _extract_items(r)
1455
+ all_items.extend(p_items)
1456
+ p_info = _extract_page_info(r)
1457
+ total = p_info.get("totalCount", 0) if p_info else 0
1458
+ if not p_items or (total > 0 and len(all_items) >= total):
1459
+ break
1460
+ page_idx += 1
1461
+ items = all_items
1462
+ page_info = {
1463
+ "totalCount": len(all_items),
1464
+ "pageIndex": 0,
1465
+ "pageSize": len(all_items),
1466
+ }
1467
+ else:
1468
+ result = funcmodel_api.list_operations(
1469
+ client,
1470
+ operation_type.value,
1471
+ page=page,
1472
+ page_size=page_size,
1473
+ fuzzy_search=fuzzy_search,
1474
+ )
1475
+ items = _extract_items(result)
1476
+ page_info = _extract_page_info(result)
1477
+
1478
+ if not items:
1479
+ if is_tty():
1480
+ get_console().print("[dim]No operations found.[/dim]")
1481
+ else:
1482
+ typer.echo("[]")
1483
+ return
1484
+ fmt = resolve_format(fmt)
1485
+ if fmt == OutputFormat.json:
1486
+ if page_all:
1487
+ typer.echo(
1488
+ json_mod.dumps(
1489
+ {"data": items, "pageInfo": page_info},
1490
+ ensure_ascii=False,
1491
+ indent=2,
1492
+ )
1493
+ )
1494
+ else:
1495
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1496
+ return
1497
+ flat_items = _flatten_operations(items)
1498
+ _print_table(
1499
+ flat_items,
1500
+ title=f"Operations ({operation_type.value})",
1501
+ page_info=page_info,
1502
+ )
1503
+ except DWFError as exc:
1504
+ handle_error(exc)
1505
+
1506
+
1507
+ @_operations_sub.command(name="all")
1508
+ def list_all_operations(
1509
+ ctx: typer.Context,
1510
+ fmt: Annotated[
1511
+ OutputFormat,
1512
+ typer.Option("--format", "-f", help="Output format: table/json"),
1513
+ ] = OutputFormat.table,
1514
+ ) -> None:
1515
+ try:
1516
+ client = get_client(ctx)
1517
+ result = funcmodel_api.get_all_operations(client)
1518
+ items = result.get("data", result)
1519
+ fmt = resolve_format(fmt)
1520
+ if fmt == OutputFormat.json:
1521
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1522
+ return
1523
+ if isinstance(items, dict):
1524
+ _print_table([items], title="All Operations")
1525
+ else:
1526
+ get_console().print(str(result))
1527
+ except DWFError as exc:
1528
+ handle_error(exc)
1529
+
1530
+
1531
+ @_operations_sub.command(name="keywords")
1532
+ def get_operation_keywords(
1533
+ ctx: typer.Context,
1534
+ fmt: Annotated[
1535
+ OutputFormat,
1536
+ typer.Option("--format", "-f", help="Output format: table/json"),
1537
+ ] = OutputFormat.table,
1538
+ ) -> None:
1539
+ try:
1540
+ client = get_client(ctx)
1541
+ result = funcmodel_api.get_operation_keywords(client)
1542
+ items = result.get("data", result)
1543
+ fmt = resolve_format(fmt)
1544
+ if fmt == OutputFormat.json:
1545
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1546
+ return
1547
+ if isinstance(items, list):
1548
+ for kw in items:
1549
+ get_console().print(kw)
1550
+ else:
1551
+ get_console().print(str(result))
1552
+ except DWFError as exc:
1553
+ handle_error(exc)
1554
+
1555
+
1556
+ @_operations_sub.command(name="get")
1557
+ def get_operation(
1558
+ ctx: typer.Context,
1559
+ oid: Annotated[str, typer.Argument(help="Operation ID")],
1560
+ fmt: Annotated[
1561
+ OutputFormat,
1562
+ typer.Option("--format", "-f", help="Output format: table/json"),
1563
+ ] = OutputFormat.table,
1564
+ ) -> None:
1565
+ try:
1566
+ client = get_client(ctx)
1567
+ result = funcmodel_api.get_operation_by_id(client, oid)
1568
+ item = result.get("data", result)
1569
+ fmt = resolve_format(fmt)
1570
+ if fmt == OutputFormat.json:
1571
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1572
+ return
1573
+ if isinstance(item, dict):
1574
+ _print_table([item], title=f"Operation ({oid})")
1575
+ else:
1576
+ get_console().print(str(result))
1577
+ except DWFError as exc:
1578
+ handle_error(exc)
1579
+
1580
+
1581
+ @_operations_sub.command(name="get-by-ids")
1582
+ def get_operations_by_ids(
1583
+ ctx: typer.Context,
1584
+ ids: Annotated[
1585
+ list[str],
1586
+ typer.Option("--id", help="Operation ID (repeatable)"),
1587
+ ],
1588
+ fmt: Annotated[
1589
+ OutputFormat,
1590
+ typer.Option("--format", "-f", help="Output format: table/json"),
1591
+ ] = OutputFormat.table,
1592
+ ) -> None:
1593
+ try:
1594
+ client = get_client(ctx)
1595
+ result = funcmodel_api.get_operations_by_ids(client, ids)
1596
+ items = result.get("data", result)
1597
+ fmt = resolve_format(fmt)
1598
+ if fmt == OutputFormat.json:
1599
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1600
+ return
1601
+ if isinstance(items, dict):
1602
+ flat = (
1603
+ [{"id": k, **v} for k, v in items.items()]
1604
+ if isinstance(items, dict)
1605
+ else items
1606
+ )
1607
+ _print_table(flat, title="Operations")
1608
+ else:
1609
+ get_console().print(str(result))
1610
+ except DWFError as exc:
1611
+ handle_error(exc)
1612
+
1613
+
1614
+ @_operations_sub.command(name="create")
1615
+ def create_operation(
1616
+ ctx: typer.Context,
1617
+ data: Annotated[
1618
+ str | None,
1619
+ typer.Option("--data", "-d", help="Operation data as JSON string"),
1620
+ ] = None,
1621
+ data_file: Annotated[
1622
+ Path | None,
1623
+ typer.Option("--data-file", help="JSON file path (alternative to --data)"),
1624
+ ] = None,
1625
+ auto_generate_name: Annotated[
1626
+ bool,
1627
+ typer.Option("--auto-name", help="Auto-generate operation name"),
1628
+ ] = False,
1629
+ dry_run: Annotated[
1630
+ bool, typer.Option("--dry-run", help="Preview without executing")
1631
+ ] = False,
1632
+ fmt: Annotated[
1633
+ OutputFormat,
1634
+ typer.Option("--format", "-f", help="Output format: table/json"),
1635
+ ] = OutputFormat.table,
1636
+ ) -> None:
1637
+ try:
1638
+ try:
1639
+ parsed = _resolve_payload(data, data_file)
1640
+ _stringify_op_fields(parsed)
1641
+ except json_mod.JSONDecodeError as e:
1642
+ get_console().print(f"[red]Invalid JSON: {e}[/red]")
1643
+ raise typer.Exit(code=2)
1644
+ if dry_run:
1645
+ typer.echo(
1646
+ json_mod.dumps(
1647
+ {"action": "create", "resource": "operation", "target": parsed},
1648
+ ensure_ascii=False,
1649
+ indent=2,
1650
+ )
1651
+ )
1652
+ raise typer.Exit(code=10)
1653
+ client = get_client(ctx)
1654
+ result = funcmodel_api.create_operation(
1655
+ client, parsed, auto_generate_name=auto_generate_name
1656
+ )
1657
+ raise_if_not_success(result, "Create operation")
1658
+ report_success(SuccessResult("Created", "operation", data=result), fmt)
1659
+ except DWFError as exc:
1660
+ handle_error(exc)
1661
+
1662
+
1663
+ @_operations_sub.command(name="update")
1664
+ def update_operation(
1665
+ ctx: typer.Context,
1666
+ data: Annotated[
1667
+ str | None,
1668
+ typer.Option("--data", "-d", help="Operation data as JSON string"),
1669
+ ] = None,
1670
+ data_file: Annotated[
1671
+ Path | None,
1672
+ typer.Option("--data-file", help="JSON file path (alternative to --data)"),
1673
+ ] = None,
1674
+ force_update: Annotated[
1675
+ bool,
1676
+ typer.Option("--force", help="Force update"),
1677
+ ] = False,
1678
+ dry_run: Annotated[
1679
+ bool, typer.Option("--dry-run", help="Preview without executing")
1680
+ ] = False,
1681
+ fmt: Annotated[
1682
+ OutputFormat,
1683
+ typer.Option("--format", "-f", help="Output format: table/json"),
1684
+ ] = OutputFormat.table,
1685
+ ) -> None:
1686
+ try:
1687
+ try:
1688
+ parsed = _resolve_payload(data, data_file)
1689
+ _stringify_op_fields(parsed)
1690
+ except json_mod.JSONDecodeError as e:
1691
+ get_console().print(f"[red]Invalid JSON: {e}[/red]")
1692
+ raise typer.Exit(code=2)
1693
+ if dry_run:
1694
+ typer.echo(
1695
+ json_mod.dumps(
1696
+ {"action": "update", "resource": "operation", "target": parsed},
1697
+ ensure_ascii=False,
1698
+ indent=2,
1699
+ )
1700
+ )
1701
+ raise typer.Exit(code=10)
1702
+ client = get_client(ctx)
1703
+ result = funcmodel_api.update_operation(
1704
+ client, parsed, force_update=force_update
1705
+ )
1706
+ raise_if_not_success(result, "Update operation")
1707
+ report_success(SuccessResult("Updated", "operation", data=result), fmt)
1708
+ except DWFError as exc:
1709
+ handle_error(exc)
1710
+
1711
+
1712
+ @_operations_sub.command(name="delete")
1713
+ def delete_operation(
1714
+ ctx: typer.Context,
1715
+ oid: Annotated[str, typer.Argument(help="Operation ID")],
1716
+ dry_run: Annotated[
1717
+ bool, typer.Option("--dry-run", help="Preview without executing")
1718
+ ] = False,
1719
+ fmt: Annotated[
1720
+ OutputFormat,
1721
+ typer.Option("--format", "-f", help="Output format: table/json"),
1722
+ ] = OutputFormat.table,
1723
+ ) -> None:
1724
+ try:
1725
+ if dry_run:
1726
+ typer.echo(
1727
+ json_mod.dumps(
1728
+ {
1729
+ "action": "delete",
1730
+ "resource": "operation",
1731
+ "target": {"id": oid},
1732
+ },
1733
+ ensure_ascii=False,
1734
+ indent=2,
1735
+ )
1736
+ )
1737
+ raise typer.Exit(code=10)
1738
+ client = get_client(ctx)
1739
+ result = funcmodel_api.delete_operation(client, oid)
1740
+ raise_if_not_success(result, "Delete operation")
1741
+ report_success(
1742
+ SuccessResult("Deleted", "operation", detail=oid, data=result), fmt
1743
+ )
1744
+ except DWFError as exc:
1745
+ handle_error(exc)
1746
+
1747
+
1748
+ @_operations_sub.command(name="move")
1749
+ def move_operation(
1750
+ ctx: typer.Context,
1751
+ oid: Annotated[str, typer.Argument(help="Operation ID to move")],
1752
+ parent_id: Annotated[
1753
+ str, typer.Option("--parent-id", help="Target parent operation/module class ID")
1754
+ ],
1755
+ dry_run: Annotated[
1756
+ bool, typer.Option("--dry-run", help="Preview without executing")
1757
+ ] = False,
1758
+ fmt: Annotated[
1759
+ OutputFormat,
1760
+ typer.Option("--format", "-f", help="Output format: table/json"),
1761
+ ] = OutputFormat.table,
1762
+ ) -> None:
1763
+ try:
1764
+ if dry_run:
1765
+ typer.echo(
1766
+ json_mod.dumps(
1767
+ {
1768
+ "action": "move",
1769
+ "resource": "operation",
1770
+ "target": {"id": oid, "parentId": parent_id},
1771
+ },
1772
+ ensure_ascii=False,
1773
+ indent=2,
1774
+ )
1775
+ )
1776
+ raise typer.Exit(code=10)
1777
+ client = get_client(ctx)
1778
+ result = funcmodel_api.move_operation(client, oid, parent_id)
1779
+ raise_if_not_success(result, "Move operation")
1780
+ report_success(
1781
+ SuccessResult("Moved", "operation", detail=oid, data=result), fmt
1782
+ )
1783
+ except DWFError as exc:
1784
+ handle_error(exc)
1785
+
1786
+
1787
+ @_operations_sub.command(name="hide")
1788
+ def hide_operation(
1789
+ ctx: typer.Context,
1790
+ oid: Annotated[str, typer.Argument(help="Operation ID")],
1791
+ hide: Annotated[
1792
+ bool, typer.Option("--hide/--show", help="Hide or show the operation")
1793
+ ] = True,
1794
+ dry_run: Annotated[
1795
+ bool, typer.Option("--dry-run", help="Preview without executing")
1796
+ ] = False,
1797
+ fmt: Annotated[
1798
+ OutputFormat,
1799
+ typer.Option("--format", "-f", help="Output format: table/json"),
1800
+ ] = OutputFormat.table,
1801
+ ) -> None:
1802
+ try:
1803
+ if dry_run:
1804
+ typer.echo(
1805
+ json_mod.dumps(
1806
+ {
1807
+ "action": "hide" if hide else "show",
1808
+ "resource": "operation",
1809
+ "target": {"id": oid},
1810
+ },
1811
+ ensure_ascii=False,
1812
+ indent=2,
1813
+ )
1814
+ )
1815
+ raise typer.Exit(code=10)
1816
+ client = get_client(ctx)
1817
+ result = funcmodel_api.hide_operation(client, oid, hide)
1818
+ raise_if_not_success(result, "Hide operation")
1819
+ action_label = "Hidden" if hide else "Shown"
1820
+ report_success(
1821
+ SuccessResult(action_label, "operation", detail=oid, data=result), fmt
1822
+ )
1823
+ except DWFError as exc:
1824
+ handle_error(exc)
1825
+
1826
+
1827
+ @_operations_sub.command(name="check-name")
1828
+ def check_operation_display_name(
1829
+ ctx: typer.Context,
1830
+ data: Annotated[
1831
+ str | None,
1832
+ typer.Option(
1833
+ "--data", "-d", help="Operation data as JSON string for display name check"
1834
+ ),
1835
+ ] = None,
1836
+ data_file: Annotated[
1837
+ Path | None,
1838
+ typer.Option("--data-file", help="JSON file path (alternative to --data)"),
1839
+ ] = None,
1840
+ fmt: Annotated[
1841
+ OutputFormat,
1842
+ typer.Option("--format", "-f", help="Output format: table/json"),
1843
+ ] = OutputFormat.table,
1844
+ ) -> None:
1845
+ try:
1846
+ try:
1847
+ parsed = _resolve_payload(data, data_file)
1848
+ except json_mod.JSONDecodeError as e:
1849
+ get_console().print(f"[red]Invalid JSON: {e}[/red]")
1850
+ raise typer.Exit(code=2)
1851
+ client = get_client(ctx)
1852
+ result = funcmodel_api.check_operation_display_name(client, parsed)
1853
+ fmt = resolve_format(fmt)
1854
+ if fmt == OutputFormat.json:
1855
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1856
+ return
1857
+ items = result.get("data", result)
1858
+ if isinstance(items, list):
1859
+ _print_table(items, title="Display Name Check Results")
1860
+ else:
1861
+ get_console().print(str(result))
1862
+ except DWFError as exc:
1863
+ handle_error(exc)
1864
+
1865
+
1866
+ @_operations_sub.command(name="global-scripts")
1867
+ def get_global_implement_scripts(
1868
+ ctx: typer.Context,
1869
+ is_client_script: Annotated[
1870
+ bool,
1871
+ typer.Option("--client/--server", help="Client-side or server-side script"),
1872
+ ] = True,
1873
+ fmt: Annotated[
1874
+ OutputFormat,
1875
+ typer.Option("--format", "-f", help="Output format: table/json"),
1876
+ ] = OutputFormat.table,
1877
+ ) -> None:
1878
+ try:
1879
+ client = get_client(ctx)
1880
+ result = funcmodel_api.get_global_implement_scripts(
1881
+ client, is_client_script=is_client_script
1882
+ )
1883
+ fmt = resolve_format(fmt)
1884
+ if fmt == OutputFormat.json:
1885
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1886
+ return
1887
+ get_console().print(str(result))
1888
+ except DWFError as exc:
1889
+ handle_error(exc)
1890
+
1891
+
1892
+ # ── Modules Commands ────────────────────────────────────────────────────
1893
+
1894
+
1895
+ @_modules_sub.command(name="list")
1896
+ def list_modules(
1897
+ ctx: typer.Context,
1898
+ app_id: Annotated[
1899
+ str | None,
1900
+ typer.Option("--app-id", help="App OID (default app if not specified)"),
1901
+ ] = None,
1902
+ with_default_module: Annotated[
1903
+ bool,
1904
+ typer.Option("--with-default", help="Include default module"),
1905
+ ] = False,
1906
+ fmt: Annotated[
1907
+ OutputFormat,
1908
+ typer.Option("--format", "-f", help="Output format: table/json"),
1909
+ ] = OutputFormat.table,
1910
+ ) -> None:
1911
+ try:
1912
+ client = get_client(ctx)
1913
+ result = funcmodel_api.list_modules(
1914
+ client, app_id=app_id, with_default_module=with_default_module
1915
+ )
1916
+ fmt = resolve_format(fmt)
1917
+ if fmt == OutputFormat.json:
1918
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1919
+ return
1920
+ data = result.get("data", result)
1921
+ if isinstance(data, list) and data:
1922
+ _print_table(data, title="Modules")
1923
+ else:
1924
+ get_console().print("[dim]No modules found.[/dim]")
1925
+ except DWFError as exc:
1926
+ handle_error(exc)
1927
+
1928
+
1929
+ @_modules_sub.command(name="tree")
1930
+ def modules_tree(
1931
+ ctx: typer.Context,
1932
+ app_id: Annotated[
1933
+ str | None,
1934
+ typer.Option("--app-id", help="App OID (all apps if not specified)"),
1935
+ ] = None,
1936
+ with_default_module: Annotated[
1937
+ bool,
1938
+ typer.Option("--with-default", help="Include default module"),
1939
+ ] = False,
1940
+ published_only: Annotated[
1941
+ bool,
1942
+ typer.Option("--published-only/--all", help="Only published apps"),
1943
+ ] = True,
1944
+ with_hidden: Annotated[
1945
+ bool,
1946
+ typer.Option("--with-hidden", help="Include hidden operations"),
1947
+ ] = False,
1948
+ fmt: Annotated[
1949
+ OutputFormat,
1950
+ typer.Option("--format", "-f", help="Output format: table/json"),
1951
+ ] = OutputFormat.table,
1952
+ ) -> None:
1953
+ try:
1954
+ client = get_client(ctx)
1955
+ result = funcmodel_api.get_modules_operations_tree(
1956
+ client,
1957
+ app_id=app_id,
1958
+ with_default_module=with_default_module,
1959
+ published_only=published_only,
1960
+ with_hidden=with_hidden,
1961
+ )
1962
+ fmt = resolve_format(fmt)
1963
+ if fmt == OutputFormat.json:
1964
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1965
+ return
1966
+ get_console().print(str(result))
1967
+ except DWFError as exc:
1968
+ handle_error(exc)
1969
+
1970
+
1971
+ @_modules_sub.command(name="operations")
1972
+ def module_operations(
1973
+ ctx: typer.Context,
1974
+ module_name: Annotated[str, typer.Argument(help="Module class name")],
1975
+ with_hidden: Annotated[
1976
+ bool,
1977
+ typer.Option("--with-hidden", help="Include hidden operations"),
1978
+ ] = False,
1979
+ fmt: Annotated[
1980
+ OutputFormat,
1981
+ typer.Option("--format", "-f", help="Output format: table/json"),
1982
+ ] = OutputFormat.table,
1983
+ ) -> None:
1984
+ try:
1985
+ client = get_client(ctx)
1986
+ result = funcmodel_api.get_module_operations(
1987
+ client, module_name, with_hidden=with_hidden
1988
+ )
1989
+ fmt = resolve_format(fmt)
1990
+ if fmt == OutputFormat.json:
1991
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
1992
+ return
1993
+ inner = result.get("data", result)
1994
+ ops = inner.get("queryOprConfigs", inner) if isinstance(inner, dict) else inner
1995
+ if isinstance(ops, list) and ops:
1996
+ _print_table(ops, title=f"Module Operations ({module_name})")
1997
+ else:
1998
+ get_console().print("[dim]No operations found.[/dim]")
1999
+ except DWFError as exc:
2000
+ handle_error(exc)
2001
+
2002
+
2003
+ @_modules_sub.command(name="update-order")
2004
+ def update_modules_order(
2005
+ ctx: typer.Context,
2006
+ app_id: Annotated[str, typer.Argument(help="App ID")],
2007
+ data: Annotated[
2008
+ str | None,
2009
+ typer.Option("--data", "-d", help="Tree data as JSON string"),
2010
+ ] = None,
2011
+ data_file: Annotated[
2012
+ Path | None,
2013
+ typer.Option("--data-file", help="JSON file path"),
2014
+ ] = None,
2015
+ dry_run: Annotated[
2016
+ bool, typer.Option("--dry-run", help="Preview without executing")
2017
+ ] = False,
2018
+ fmt: Annotated[
2019
+ OutputFormat,
2020
+ typer.Option("--format", "-f", help="Output format: table/json"),
2021
+ ] = OutputFormat.table,
2022
+ ) -> None:
2023
+ try:
2024
+ try:
2025
+ if data_file:
2026
+ tree = json_mod.loads(data_file.read_text(encoding="utf-8"))
2027
+ elif data:
2028
+ tree = json_mod.loads(data)
2029
+ else:
2030
+ raise typer.BadParameter("Provide --data or --data-file")
2031
+ except json_mod.JSONDecodeError as e:
2032
+ get_console().print(f"[red]Invalid JSON: {e}[/red]")
2033
+ raise typer.Exit(code=2)
2034
+ if dry_run:
2035
+ typer.echo(
2036
+ json_mod.dumps(
2037
+ {
2038
+ "action": "update-order",
2039
+ "resource": "modules-tree",
2040
+ "target": {"appId": app_id, "tree": tree},
2041
+ },
2042
+ ensure_ascii=False,
2043
+ indent=2,
2044
+ )
2045
+ )
2046
+ raise typer.Exit(code=10)
2047
+ client = get_client(ctx)
2048
+ result = funcmodel_api.update_modules_tree_order(client, app_id, tree)
2049
+ raise_if_not_success(result, "Update modules tree order")
2050
+ report_success(
2051
+ SuccessResult("Updated", "modules tree order", detail=app_id, data=result),
2052
+ fmt,
2053
+ )
2054
+ except DWFError as exc:
2055
+ handle_error(exc)