asecli 0.6.2__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 (98) hide show
  1. asecli/__init__.py +3 -0
  2. asecli/bridge/__init__.py +40 -0
  3. asecli/bridge/_editor_port_contract.py +131 -0
  4. asecli/bridge/_editor_primitives.py +205 -0
  5. asecli/bridge/_editor_spec_grammar.py +245 -0
  6. asecli/bridge/_editor_spec_io.py +26 -0
  7. asecli/bridge/_editor_spec_validation.py +218 -0
  8. asecli/bridge/_gui_handoff_store.py +78 -0
  9. asecli/bridge/_gui_project.py +35 -0
  10. asecli/bridge/_gui_resource_store.py +159 -0
  11. asecli/bridge/_gui_resource_upgrade.py +122 -0
  12. asecli/bridge/editor_create.py +173 -0
  13. asecli/bridge/editor_spec.py +250 -0
  14. asecli/bridge/graph_geometry.py +249 -0
  15. asecli/bridge/graph_geometry_parser.py +57 -0
  16. asecli/bridge/graph_inspect.py +136 -0
  17. asecli/bridge/gui_handoff.py +100 -0
  18. asecli/bridge/gui_presentation.py +70 -0
  19. asecli/bridge/gui_provider_detection.py +197 -0
  20. asecli/bridge/gui_runtime_probe.py +25 -0
  21. asecli/bridge/gui_support.py +240 -0
  22. asecli/bridge/gui_support_resource.py +58 -0
  23. asecli/bridge/mcp_client.py +231 -0
  24. asecli/bridge/recompile.py +107 -0
  25. asecli/bridge/resource_text.py +12 -0
  26. asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +238 -0
  27. asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +235 -0
  28. asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +125 -0
  29. asecli/bridge/resources/asecli_material_gui.condition.cs.txt +99 -0
  30. asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +105 -0
  31. asecli/bridge/resources/asecli_material_gui.part00.cs.txt +300 -0
  32. asecli/bridge/resources/asecli_material_gui.part01.cs.txt +290 -0
  33. asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +174 -0
  34. asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +108 -0
  35. asecli/bridge/resources/editor_create.part00.cs.txt +176 -0
  36. asecli/bridge/resources/editor_create.part01.cs.txt +145 -0
  37. asecli/bridge/resources/editor_create.part02.cs.txt +139 -0
  38. asecli/bridge/resources/editor_create.part03.cs.txt +161 -0
  39. asecli/bridge/resources/editor_create.part04.cs.txt +114 -0
  40. asecli/bridge/resources/wire_route.transaction.cs.txt +140 -0
  41. asecli/bridge/wire_route.py +127 -0
  42. asecli/checks/__init__.py +10 -0
  43. asecli/checks/checksum.py +53 -0
  44. asecli/checks/local_vars.py +135 -0
  45. asecli/checks/usage.py +141 -0
  46. asecli/checks/validate.py +129 -0
  47. asecli/cli/__init__.py +1 -0
  48. asecli/cli/commands.py +237 -0
  49. asecli/cli/commentary_command.py +158 -0
  50. asecli/cli/create_command.py +250 -0
  51. asecli/cli/custom_gui_command.py +170 -0
  52. asecli/cli/gui_provider.py +30 -0
  53. asecli/cli/gui_support_command.py +35 -0
  54. asecli/cli/io.py +214 -0
  55. asecli/cli/layout_command.py +213 -0
  56. asecli/cli/main.py +245 -0
  57. asecli/cli/recompile_metadata.py +53 -0
  58. asecli/cli/skill_command.py +121 -0
  59. asecli/cli/usage_command.py +42 -0
  60. asecli/core/__init__.py +84 -0
  61. asecli/core/comment_bounds.py +166 -0
  62. asecli/core/comment_layout.py +167 -0
  63. asecli/core/comment_metrics.py +132 -0
  64. asecli/core/comment_purpose.py +140 -0
  65. asecli/core/commentary.py +225 -0
  66. asecli/core/compiled_metadata.py +135 -0
  67. asecli/core/compiled_properties.py +190 -0
  68. asecli/core/custom_gui.py +250 -0
  69. asecli/core/custom_gui_versions.py +35 -0
  70. asecli/core/fishbone_placement.py +159 -0
  71. asecli/core/fishbone_topology.py +186 -0
  72. asecli/core/graph_ops.py +122 -0
  73. asecli/core/layout.py +174 -0
  74. asecli/core/layout_audit.py +237 -0
  75. asecli/core/layout_audit_geometry.py +247 -0
  76. asecli/core/layout_audit_repeated.py +44 -0
  77. asecli/core/layout_graph.py +113 -0
  78. asecli/core/local_vars.py +54 -0
  79. asecli/core/material_gui_condition.py +68 -0
  80. asecli/core/material_gui_protocol.py +35 -0
  81. asecli/core/material_gui_spec.py +191 -0
  82. asecli/core/meticulous_layout.py +114 -0
  83. asecli/core/model.py +237 -0
  84. asecli/core/property_presentation.py +233 -0
  85. asecli/core/wire_geometry.py +83 -0
  86. asecli/core/wire_router.py +248 -0
  87. asecli/schema/__init__.py +49 -0
  88. asecli/schema/data/observed.json +16 -0
  89. asecli/schema/data/schemas.json +14106 -0
  90. asecli/skills/asecli/SKILL.md +275 -0
  91. asecli/skills/asecli/references/layout-standard.md +99 -0
  92. asecli/skills/asecli/references/master-output-settings-standard.md +51 -0
  93. asecli/skills/asecli/references/material-property-standard.md +108 -0
  94. asecli-0.6.2.dist-info/METADATA +594 -0
  95. asecli-0.6.2.dist-info/RECORD +98 -0
  96. asecli-0.6.2.dist-info/WHEEL +4 -0
  97. asecli-0.6.2.dist-info/entry_points.txt +2 -0
  98. asecli-0.6.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,250 @@
1
+ """Safe shader creation and semantic Shader-name mutation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import re
8
+
9
+ from ..bridge import (
10
+ MZGUI_EDITOR,
11
+ McpError,
12
+ SpecError,
13
+ create_shader_via_mcp,
14
+ install_gui_support,
15
+ load_editor_graph_spec,
16
+ route_create_backend,
17
+ )
18
+ from ..checks import ChecksumFormatError, fix_checksum, validate_file
19
+ from ..core import (
20
+ AseFile,
21
+ apply_material_gui_spec,
22
+ main_master_node,
23
+ require_property_presentation,
24
+ sync_compiled_property_metadata,
25
+ )
26
+ from .commands import CliError, _commit_text, _load
27
+ from .gui_provider import select_editor_provider
28
+ from .io import UnsafeWritePathError, file_digest
29
+
30
+
31
+ _SHADER_DECLARATION = re.compile(r'(?m)^(?P<indent>[ \t]*)Shader[ \t]+"(?P<name>[^"\r\n]+)"')
32
+ _TEMPLATE_NAME_FIELD = {
33
+ "AmplifyShaderEditor.TemplateMultiPassMasterNode": 12,
34
+ "AmplifyShaderEditor.TemplateMasterNode": 12,
35
+ }
36
+
37
+
38
+ def cmd_create(args) -> dict:
39
+ spec = None
40
+ if args.spec:
41
+ try:
42
+ spec = load_editor_graph_spec(args.spec)
43
+ except FileNotFoundError as exc:
44
+ raise CliError("NOT_FOUND", str(exc)) from exc
45
+ except SpecError as exc:
46
+ raise CliError("USAGE_ERROR", str(exc)) from exc
47
+ try:
48
+ backend = route_create_backend(args.backend, spec)
49
+ except SpecError as exc:
50
+ raise CliError("USAGE_ERROR", str(exc)) from exc
51
+ if backend == "editor":
52
+ return _cmd_create_editor(args, spec)
53
+ return _cmd_create_text(args, spec)
54
+
55
+
56
+ def _cmd_create_text(args, spec) -> dict:
57
+ if spec is not None:
58
+ raise CliError("USAGE_ERROR", "--spec requires --backend editor or auto")
59
+ if not args.from_template:
60
+ raise CliError("USAGE_ERROR", "--from is required for the text create backend")
61
+ f = _load(args.from_template)
62
+ if args.graph_from:
63
+ donor = _load(args.graph_from)
64
+ f.replace_graph(donor.graph)
65
+ if args.name:
66
+ _set_shader_name(f, args.name)
67
+ try:
68
+ text = fix_checksum(f.serialize())
69
+ except ChecksumFormatError as exc:
70
+ raise CliError("CHECKSUM_FORMAT_ERROR", str(exc)) from exc
71
+ try:
72
+ created = AseFile.from_text(text)
73
+ errors = [issue for issue in validate_file(created) if issue["severity"] == "error"]
74
+ except ValueError as exc:
75
+ raise CliError("VALIDATION_ERROR", f"created shader is not parseable: {exc}") from exc
76
+ if errors:
77
+ raise CliError(
78
+ "VALIDATION_ERROR",
79
+ f"created shader contains {len(errors)} structural error(s)",
80
+ {"issues": errors, "error_count": len(errors)},
81
+ )
82
+ try:
83
+ presentation = require_property_presentation(created)
84
+ except ValueError as exc:
85
+ raise CliError("PROPERTY_PRESENTATION_ERROR", str(exc)) from exc
86
+ try:
87
+ output_digest = file_digest(args.out)
88
+ except FileNotFoundError:
89
+ output_digest = None
90
+ except UnsafeWritePathError as exc:
91
+ raise CliError("UNSAFE_PATH", str(exc)) from exc
92
+ if output_digest is not None and not args.force:
93
+ raise CliError("USAGE_ERROR", f"{args.out} exists (use --force)")
94
+ _commit_text(args.out, text, output_digest)
95
+ return {
96
+ "created": args.out,
97
+ "name": args.name,
98
+ "graph_from": args.graph_from,
99
+ "backend": "text",
100
+ "property_presentation": presentation,
101
+ }
102
+
103
+
104
+ def _cmd_create_editor(args, spec) -> dict:
105
+ if spec is None:
106
+ raise CliError("USAGE_ERROR", "--spec is required for the editor create backend")
107
+ if spec.version not in {2, 3}:
108
+ raise CliError(
109
+ "USAGE_ERROR",
110
+ "CLI Editor creation requires EditorGraphSpec v2 or v3 with Chinese inspector_name and help for every property",
111
+ )
112
+ if args.from_template or args.graph_from or args.name:
113
+ raise CliError("USAGE_ERROR", "--from, --graph-from and --name are text-backend options")
114
+ if args.force:
115
+ raise CliError("USAGE_ERROR", "editor create only supports absent targets; --force is not allowed")
116
+ if args.instance_token_argv is not None:
117
+ raise CliError("USAGE_ERROR", "do not pass MCP tokens via argv; use ASECLI_MCP_INSTANCE_TOKEN")
118
+ try:
119
+ selected_editor, gui_support = select_editor_provider(args.out)
120
+ except (FileNotFoundError, ValueError, RuntimeError, OSError) as exc:
121
+ raise CliError("GUI_SUPPORT_ERROR", str(exc)) from exc
122
+ try:
123
+ result = create_shader_via_mcp(
124
+ args.out,
125
+ spec,
126
+ mcp_url=args.mcp_url,
127
+ instance_token=os.environ.get("ASECLI_MCP_INSTANCE_TOKEN"),
128
+ allow_remote_mcp=args.allow_remote_mcp,
129
+ )
130
+ except McpError as exc:
131
+ raise CliError("BRIDGE_ERROR", str(exc), exc.data) from exc
132
+ except FileExistsError as exc:
133
+ raise CliError("USAGE_ERROR", str(exc)) from exc
134
+ except FileNotFoundError as exc:
135
+ raise CliError("NOT_FOUND", str(exc)) from exc
136
+ except ValueError as exc:
137
+ raise CliError("USAGE_ERROR", str(exc)) from exc
138
+
139
+ try:
140
+ created = AseFile.from_path(args.out)
141
+ errors = [issue for issue in validate_file(created) if issue["severity"] == "error"]
142
+ except (OSError, UnicodeError, ValueError) as exc:
143
+ raise CliError(
144
+ "BRIDGE_ERROR",
145
+ f"Editor-created shader is not parseable and was preserved for diagnosis: {exc}",
146
+ _preserved_editor_asset_details(args.out, result),
147
+ ) from exc
148
+ if errors:
149
+ raise CliError(
150
+ "BRIDGE_ERROR",
151
+ f"Editor-created shader contains {len(errors)} structural error(s) and was preserved for diagnosis",
152
+ {
153
+ "issues": errors,
154
+ "error_count": len(errors),
155
+ **_preserved_editor_asset_details(args.out, result),
156
+ },
157
+ )
158
+ try:
159
+ output, presentation = _finalize_editor_property_presentation(
160
+ created, spec, editor=selected_editor
161
+ )
162
+ _commit_text(args.out, output, created.source_digest)
163
+ result["shader_sha256"] = file_digest(args.out)
164
+ except (OSError, UnicodeError, ValueError) as exc:
165
+ raise CliError(
166
+ "BRIDGE_ERROR",
167
+ f"Editor-created shader failed property-presentation finalization and was preserved for diagnosis: {exc}",
168
+ _preserved_editor_asset_details(args.out, result),
169
+ ) from exc
170
+ return {
171
+ "created": args.out,
172
+ "backend": "editor",
173
+ "gui_support": gui_support,
174
+ "property_presentation": presentation,
175
+ **result,
176
+ }
177
+
178
+
179
+ def _finalize_editor_property_presentation(
180
+ created: AseFile, spec, *, editor: str = MZGUI_EDITOR
181
+ ) -> tuple[str, dict]:
182
+ """Apply the same v2 presentation contract after ASE commits an Editor graph."""
183
+ presentation_spec = {
184
+ "editor": editor,
185
+ "properties": [
186
+ {
187
+ "name": node.property_name,
188
+ "display_name": node.inspector_name,
189
+ "tooltip": node.tooltip,
190
+ "help": node.help,
191
+ "enabled_if": node.enabled_if,
192
+ }
193
+ for node in spec.nodes
194
+ if node.property_name is not None
195
+ ],
196
+ }
197
+ apply_material_gui_spec(created, presentation_spec)
198
+ sync_compiled_property_metadata(created)
199
+ from asecli.core.compiled_metadata import hide_known_template_compiled_only_properties
200
+
201
+ hide_known_template_compiled_only_properties(created, spec.template.guid)
202
+ presentation = require_property_presentation(created)
203
+ return fix_checksum(created.serialize()), presentation
204
+
205
+ def _preserved_editor_asset_details(path: str, result: dict) -> dict:
206
+ """Describe a post-commit failure without deleting an identity that may have changed."""
207
+ return {
208
+ "preserved_path": str(Path(path).resolve()),
209
+ "cleanup": "skipped_untrusted_post_commit_asset",
210
+ "transaction_nonce": result.get("transaction_nonce"),
211
+ "shader_sha256": result.get("shader_sha256"),
212
+ "meta_sha256": result.get("meta_sha256"),
213
+ }
214
+
215
+
216
+ def _set_shader_name(ase_file: AseFile, name: str) -> None:
217
+ if not name.strip() or len(name) > 255:
218
+ raise CliError("USAGE_ERROR", "shader name must be 1-255 non-whitespace characters")
219
+ if any(ord(char) < 32 for char in name) or any(char in name for char in ('"', "'", ";", "\\")):
220
+ raise CliError("USAGE_ERROR", "shader name contains a forbidden quote, separator, escape, or control character")
221
+ matches = list(_SHADER_DECLARATION.finditer(ase_file.prefix))
222
+ if len(matches) != 1:
223
+ raise CliError("USAGE_ERROR", f"template must contain exactly one Shader declaration, found {len(matches)}")
224
+ declaration = matches[0]
225
+ ase_file.prefix = (
226
+ ase_file.prefix[: declaration.start("name")]
227
+ + name
228
+ + ase_file.prefix[declaration.end("name") :]
229
+ )
230
+
231
+ try:
232
+ master = main_master_node(ase_file.graph)
233
+ except ValueError as exc:
234
+ if any("MasterNode" in node.type_name for node in ase_file.graph.nodes):
235
+ raise CliError("USAGE_ERROR", f"cannot resolve graph Master name field: {exc}") from exc
236
+ return
237
+ index = _TEMPLATE_NAME_FIELD.get(master.type_name)
238
+ if index is None:
239
+ old_name = declaration.group("name")
240
+ candidates = [i for i, value in enumerate(master.raw_fields) if value == old_name]
241
+ if len(candidates) != 1:
242
+ raise CliError(
243
+ "USAGE_ERROR",
244
+ f"unsupported Master name layout for {master.type_name}: found {len(candidates)} candidate fields",
245
+ )
246
+ index = candidates[0]
247
+ if index >= len(master.raw_fields):
248
+ raise CliError("USAGE_ERROR", f"Master node {master.node_id} has no shader-name field {index}")
249
+ master.raw_fields[index] = name
250
+ ase_file.graph.replace_node(master)
@@ -0,0 +1,170 @@
1
+ """CLI composition for ASE CustomEditor and property metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from ..checks import fix_checksum, validate_file
9
+ from ..core import (
10
+ ASECLI_GUI_EDITOR,
11
+ MZGUI_EDITOR,
12
+ SUPPORTED_GUI_EDITORS,
13
+ apply_material_gui_spec,
14
+ enable_if_attribute,
15
+ graph_custom_editor,
16
+ inspect_custom_gui,
17
+ remove_property_metadata_attribute,
18
+ resolve_property_node,
19
+ semantic_attribute,
20
+ set_custom_editor,
21
+ set_property_metadata_attribute,
22
+ sync_compiled_property_metadata,
23
+ )
24
+ from .commands import CliError, _commit_text, _load
25
+
26
+
27
+ def cmd_custom_gui(args) -> dict:
28
+ """Inspect or safely mutate ASE CustomEditor and ASECLI property metadata."""
29
+ if args.enabled_if is not None and args.enabled_if_value is None:
30
+ raise CliError("USAGE_ERROR", "--enabled-if requires --enabled-if-value")
31
+ if args.enabled_if is None and args.enabled_if_value is not None:
32
+ raise CliError("USAGE_ERROR", "--enabled-if-value requires --enabled-if")
33
+ if args.enabled_if is None and args.enabled_if_operator is not None:
34
+ raise CliError("USAGE_ERROR", "--enabled-if-operator requires --enabled-if")
35
+ f = _load(args.file)
36
+ changes = []
37
+ node_actions = any(
38
+ (
39
+ args.group is not None,
40
+ args.clear_group,
41
+ args.tooltip is not None,
42
+ args.clear_tooltip,
43
+ args.help_box is not None,
44
+ args.clear_help_box,
45
+ args.enabled_if is not None,
46
+ args.clear_enabled_if,
47
+ bool(args.add_attribute),
48
+ bool(args.remove_attribute),
49
+ )
50
+ )
51
+ single_mutation = node_actions or args.editor is not None or args.clear_editor
52
+ if args.spec and single_mutation:
53
+ raise CliError("USAGE_ERROR", "--spec cannot be combined with individual custom GUI mutation options")
54
+ if args.spec and (args.node is not None or args.property is not None):
55
+ raise CliError("USAGE_ERROR", "--spec cannot be combined with --node or --property")
56
+ if node_actions and args.node is None and args.property is None:
57
+ raise CliError("USAGE_ERROR", "--node or --property is required for ASECLI property metadata operations")
58
+
59
+ try:
60
+ initial_presentation = inspect_custom_gui(f)["property_presentation"]
61
+ if args.spec:
62
+ changes.extend(apply_material_gui_spec(f, _load_spec(args.spec)))
63
+ else:
64
+ if args.editor is not None:
65
+ changes.append(set_custom_editor(f, args.editor))
66
+ elif args.clear_editor:
67
+ changes.append(set_custom_editor(f, None))
68
+
69
+ target = None
70
+ if args.node is not None or args.property is not None:
71
+ target = resolve_property_node(
72
+ f.graph, node_id=args.node, property_name=args.property
73
+ ).node_id
74
+
75
+ additions: list[str] = []
76
+ if args.group is not None:
77
+ additions.append(semantic_attribute("FoldoutMzgui", args.group))
78
+ if args.tooltip is not None:
79
+ additions.append(semantic_attribute("TooltipMzgui", args.tooltip))
80
+ if args.help_box is not None:
81
+ additions.append(semantic_attribute("HelpBoxMzgui", args.help_box))
82
+ if args.enabled_if is not None:
83
+ additions.append(
84
+ enable_if_attribute(
85
+ args.enabled_if,
86
+ args.enabled_if_operator or "Equal",
87
+ args.enabled_if_value,
88
+ )
89
+ )
90
+ additions.extend(args.add_attribute or [])
91
+
92
+ if additions and graph_custom_editor(f.graph) not in SUPPORTED_GUI_EDITORS:
93
+ supported = ", ".join(sorted(SUPPORTED_GUI_EDITORS))
94
+ raise ValueError(
95
+ "MZGUI-compatible property metadata requires a selected material GUI; "
96
+ f"pass --editor with one of: {supported}"
97
+ )
98
+
99
+ removals = (
100
+ [(args.clear_group, "FoldoutMzgui"), (args.clear_tooltip, "TooltipMzgui"),
101
+ (args.clear_help_box, "HelpBoxMzgui"),
102
+ (args.clear_enabled_if, "EnableIfMzgui")]
103
+ )
104
+ for enabled, type_name in removals:
105
+ if enabled:
106
+ changes.append(remove_property_metadata_attribute(f.graph, target, type_name))
107
+ for type_name in args.remove_attribute or []:
108
+ changes.append(remove_property_metadata_attribute(f.graph, target, type_name))
109
+ for raw in additions:
110
+ changes.append(set_property_metadata_attribute(f.graph, target, raw))
111
+
112
+ if changes and graph_custom_editor(f.graph) == ASECLI_GUI_EDITOR:
113
+ changes.insert(0, set_custom_editor(f, MZGUI_EDITOR))
114
+ if changes:
115
+ changes.extend(sync_compiled_property_metadata(f))
116
+ state = inspect_custom_gui(f)
117
+ except KeyError as exc:
118
+ raise CliError("NOT_FOUND", str(exc))
119
+ except ValueError as exc:
120
+ raise CliError("CUSTOM_GUI_ERROR", str(exc))
121
+
122
+ if not changes:
123
+ if args.write:
124
+ raise CliError("USAGE_ERROR", "--write requires a custom GUI mutation option")
125
+ return {"file": args.file, "action": "inspect", "state": state, "written": False}
126
+
127
+ contract_applies = initial_presentation["claimed"] or state["property_presentation"]["claimed"]
128
+ if args.write and contract_applies and not state["property_presentation"]["valid"]:
129
+ raise CliError(
130
+ "PROPERTY_PRESENTATION_ERROR",
131
+ "ASECLI-managed property presentation contract failed: "
132
+ + ", ".join(state["property_presentation"]["violations"]),
133
+ {"property_presentation": state["property_presentation"]},
134
+ )
135
+
136
+ issues = validate_file(f)
137
+ errors = [issue for issue in issues if issue["severity"] == "error"]
138
+ if errors:
139
+ raise CliError(
140
+ "VALIDATION_ERROR",
141
+ f"mutation would leave {len(errors)} structural error(s)",
142
+ {"issues": issues, "error_count": len(errors)},
143
+ )
144
+ output = fix_checksum(f.serialize())
145
+ if args.write:
146
+ _commit_text(args.file, output, f.source_digest)
147
+ return {
148
+ "file": args.file,
149
+ "action": "mutate",
150
+ "changes": changes,
151
+ "state": state,
152
+ "requires_recompile": bool(changes),
153
+ "written": bool(args.write),
154
+ "path": args.file if args.write else None,
155
+ "preview_bytes": len(output),
156
+ "checksum_recomputed": True,
157
+ }
158
+
159
+
160
+ def _load_spec(path: str) -> dict:
161
+ try:
162
+ text = Path(path).read_text(encoding="utf-8")
163
+ except FileNotFoundError as exc:
164
+ raise KeyError(f"spec file not found: {path}") from exc
165
+ except UnicodeDecodeError as exc:
166
+ raise ValueError(f"material GUI spec is not valid UTF-8: {path}") from exc
167
+ try:
168
+ return json.loads(text)
169
+ except json.JSONDecodeError as exc:
170
+ raise ValueError(f"invalid material GUI spec JSON at line {exc.lineno}, column {exc.colno}") from exc
@@ -0,0 +1,30 @@
1
+ """Choose the single material-GUI provider for an Editor create operation."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ..bridge import MZGUI_EDITOR, install_gui_support
6
+
7
+
8
+ def select_editor_provider(shader_path: str) -> tuple[str, dict | None]:
9
+ """Install the fallback only when the enclosing Unity project lacks MZGUI.
10
+
11
+ Projectless behavior exists solely for isolated bridge test doubles; a real
12
+ Editor create target must be inside a Unity project.
13
+ """
14
+ project_root = unity_project_root(Path(shader_path).resolve())
15
+ if project_root is None:
16
+ return MZGUI_EDITOR, None
17
+ support = install_gui_support(project_root, write=True)
18
+ editor = support.get("recommended_editor")
19
+ if not isinstance(editor, str):
20
+ raise RuntimeError("material GUI provider selection did not return an editor")
21
+ return editor, support
22
+
23
+
24
+ def unity_project_root(path: Path) -> Path | None:
25
+ current = path.parent
26
+ while current != current.parent:
27
+ if (current / "Assets").is_dir() and (current / "ProjectSettings").is_dir():
28
+ return current
29
+ current = current.parent
30
+ return None
@@ -0,0 +1,35 @@
1
+ """CLI composition for the built-in ASECLI Unity material GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from ..bridge import (
8
+ McpError,
9
+ handoff_gui_support,
10
+ install_gui_support,
11
+ probe_native_mzgui_via_mcp,
12
+ )
13
+ from .commands import CliError
14
+
15
+
16
+ def cmd_gui_support(args) -> dict:
17
+ if args.instance_token_argv is not None:
18
+ raise CliError("USAGE_ERROR", "do not pass MCP tokens via argv; use ASECLI_MCP_INSTANCE_TOKEN")
19
+ try:
20
+ runtime_probe = None
21
+ if args.runtime_probe:
22
+ runtime_probe = probe_native_mzgui_via_mcp(
23
+ args.project,
24
+ mcp_url=args.mcp_url,
25
+ instance_token=os.environ.get("ASECLI_MCP_INSTANCE_TOKEN"),
26
+ allow_remote_mcp=args.allow_remote_mcp,
27
+ )
28
+ operation = handoff_gui_support if args.handoff_native else install_gui_support
29
+ return operation(args.project, write=args.write, runtime_probe=runtime_probe)
30
+ except FileNotFoundError as exc:
31
+ raise CliError("NOT_FOUND", str(exc)) from exc
32
+ except McpError as exc:
33
+ raise CliError("BRIDGE_ERROR", str(exc), exc.data) from exc
34
+ except (FileExistsError, ValueError, RuntimeError, OSError) as exc:
35
+ raise CliError("GUI_SUPPORT_ERROR", str(exc)) from exc