inscript-lang 2.11.0__tar.gz → 2.13.0__tar.gz

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 (38) hide show
  1. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/errors.py +18 -0
  3. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/export_pipeline.py +112 -1
  4. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript.py +64 -8
  5. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  6. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/SOURCES.txt +3 -0
  7. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/top_level.txt +3 -0
  8. inscript_lang-2.13.0/inscript_studio_api.py +295 -0
  9. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/pyproject.toml +2 -1
  10. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/setup.py +1 -0
  11. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_game.py +40 -0
  12. inscript_lang-2.13.0/studio_bridge.py +585 -0
  13. inscript_lang-2.13.0/studio_readiness.py +408 -0
  14. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/README.md +0 -0
  15. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/analyzer.py +0 -0
  16. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/ast_nodes.py +0 -0
  17. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/compiler.py +0 -0
  18. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/environment.py +0 -0
  19. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/hot_reload.py +0 -0
  20. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_dap.py +0 -0
  21. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_fmt.py +0 -0
  22. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  23. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  24. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/requires.txt +0 -0
  25. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_test.py +0 -0
  26. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/interpreter.py +0 -0
  27. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/lexer.py +0 -0
  28. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/parser.py +0 -0
  29. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/pygame_backend.py +0 -0
  30. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/repl.py +0 -0
  31. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/scene_tree.py +0 -0
  32. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/setup.cfg +0 -0
  33. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib.py +0 -0
  34. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_assets.py +0 -0
  35. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_extended.py +0 -0
  36. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_extended_2.py +0 -0
  37. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_values.py +0 -0
  38. {inscript_lang-2.11.0 → inscript_lang-2.13.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.11.0
3
+ Version: 2.13.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -85,6 +85,24 @@ class InScriptError(Exception):
85
85
  self.code = code or ERROR_CODES.get(cls_name, "E0000")
86
86
  super().__init__(self._format())
87
87
 
88
+ def to_dict(self) -> dict:
89
+ """v2.12.0: Structured dict for --json-errors / Studio error panel."""
90
+ return {
91
+ "type": "error",
92
+ "code": self.code,
93
+ "class": type(self).__name__,
94
+ "message": self.message,
95
+ "line": self.line,
96
+ "col": self.col,
97
+ "source_line": self.source_line,
98
+ "hint": self.hint,
99
+ "call_trace": self.call_trace,
100
+ }
101
+
102
+ def to_json(self) -> str:
103
+ import json as _j
104
+ return _j.dumps(self.to_dict(), separators=(",", ":"))
105
+
88
106
  def _format(self) -> str:
89
107
  code = self.code
90
108
  cls = type(self).__name__
@@ -42,7 +42,7 @@ MANIFEST_FILE = "inscript.toml"
42
42
  ENTRY_DEFAULT = os.path.join("src", "main.ins")
43
43
  BUILD_MANIFEST = "build.toml"
44
44
 
45
- VALID_TARGETS = ("desktop", "web", "android", "ibc")
45
+ VALID_TARGETS = ("desktop", "web", "android", "ios", "ibc")
46
46
 
47
47
  # Pyodide CDN base used in web builds
48
48
  PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.27.0/full/"
@@ -645,3 +645,114 @@ def run_build(target: str, project_dir: str = ".") -> int:
645
645
  for e in r.errors:
646
646
  print(f"[build] {e}", file=sys.stderr)
647
647
  return 0 if r.success else 1
648
+
649
+
650
+ # ─────────────────────────────────────────────────────────────────────────────
651
+ # v2.13.0: Target: iOS
652
+ # ─────────────────────────────────────────────────────────────────────────────
653
+
654
+ def build_ios(project_dir: str = ".") -> BuildResult:
655
+ """
656
+ Build an iOS app via BeeWare Briefcase.
657
+ Requires: macOS + Xcode + pip install briefcase
658
+
659
+ Output layout:
660
+ build/ios/
661
+ pyproject.toml — Briefcase config
662
+ app.py — InScript iOS entry point
663
+ build.toml — build manifest
664
+ """
665
+ import time, subprocess
666
+ t0 = time.perf_counter()
667
+ errors: List[str] = []
668
+ artifacts: List[str] = []
669
+
670
+ manifest = _read_manifest(project_dir)
671
+ pkg = manifest.get("package", {})
672
+ version = pkg.get("version", "0.0.0")
673
+ name = pkg.get("name", "mygame")
674
+ entry = pkg.get("entry", ENTRY_DEFAULT)
675
+ bundle = pkg.get("bundle", f"dev.inscript.{name}")
676
+
677
+ out_dir = os.path.join(project_dir, BUILD_DIR, "ios")
678
+ os.makedirs(out_dir, exist_ok=True)
679
+
680
+ bp_path = os.path.join(out_dir, "pyproject.toml")
681
+ with open(bp_path, "w") as f:
682
+ f.write(textwrap.dedent(f"""\
683
+ [tool.briefcase]
684
+ project_name = "{name}"
685
+ bundle = "{bundle}"
686
+ version = "{version}"
687
+ description = "Built with InScript"
688
+ license = "MIT"
689
+
690
+ [tool.briefcase.app.{name}]
691
+ formal_name = "{name}"
692
+ description = "Built with InScript"
693
+ sources = ["."]
694
+ requires = ["inscript-lang"]
695
+
696
+ [tool.briefcase.app.{name}.iOS]
697
+ requires = ["toga-iOS"]
698
+ """))
699
+ artifacts.append(bp_path)
700
+
701
+ app_py = os.path.join(out_dir, "app.py")
702
+ with open(app_py, "w") as f:
703
+ f.write(textwrap.dedent(f"""\
704
+ # InScript iOS entry — generated by inscript build --target ios
705
+ import sys
706
+ try:
707
+ import inscript
708
+ inscript.main(["{entry}"])
709
+ except Exception as e:
710
+ print(f"InScript launch error: {{e}}", file=sys.stderr)
711
+ """))
712
+ artifacts.append(app_py)
713
+
714
+ for sub in ("src", "assets", "scenes"):
715
+ s = os.path.join(project_dir, sub)
716
+ d = os.path.join(out_dir, sub)
717
+ if os.path.isdir(s):
718
+ if os.path.exists(d): shutil.rmtree(d)
719
+ shutil.copytree(s, d)
720
+
721
+ from inscript import VERSION as _IV
722
+ bm = _write_build_manifest(out_dir, {
723
+ "target": "ios", "version": version, "entry": entry,
724
+ "inscript_version": _IV, "bundle": bundle,
725
+ "artifacts": {"pyproject": "pyproject.toml", "app": "app.py"},
726
+ "asset_hashes": {},
727
+ })
728
+ artifacts.append(bm)
729
+
730
+ # Check platform and briefcase
731
+ if sys.platform != "darwin":
732
+ msg = "iOS builds require macOS. Current platform: " + sys.platform
733
+ errors.append(msg)
734
+ print(f"[build:ios] ⚠ {msg}")
735
+ elapsed = (time.perf_counter() - t0) * 1000
736
+ return BuildResult("ios", False, out_dir, artifacts, errors, elapsed)
737
+
738
+ briefcase_ok = shutil.which("briefcase") is not None
739
+ if not briefcase_ok:
740
+ msg = ("BeeWare Briefcase not installed. "
741
+ "Install: pip install briefcase "
742
+ "Then: cd build/ios && briefcase create iOS && briefcase build iOS")
743
+ errors.append(msg)
744
+ print(f"[build:ios] ⚠ {msg}")
745
+ elapsed = (time.perf_counter() - t0) * 1000
746
+ return BuildResult("ios", False, out_dir, artifacts, errors, elapsed)
747
+
748
+ try:
749
+ subprocess.run([sys.executable, "-m", "briefcase", "create", "iOS"],
750
+ cwd=out_dir, check=True)
751
+ subprocess.run([sys.executable, "-m", "briefcase", "build", "iOS"],
752
+ cwd=out_dir, check=True)
753
+ print(f"[build:ios] ✅ iOS app built → {out_dir}")
754
+ except subprocess.CalledProcessError as e:
755
+ errors.append(f"briefcase failed: {e}")
756
+
757
+ elapsed = (time.perf_counter() - t0) * 1000
758
+ return BuildResult("ios", len(errors) == 0, out_dir, artifacts, errors, elapsed)
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.11.0"
27
+ VERSION = "2.13.0"
28
28
 
29
29
  MANIFEST_FILENAME = "inscript.toml"
30
30
  LOCK_FILENAME = "inscript.lock"
@@ -2385,21 +2385,34 @@ def run_source(source: str, filename: str = "<stdin>",
2385
2385
  no_warn_unused: bool = False,
2386
2386
  warn_as_error: bool = False,
2387
2387
  strict: bool = False,
2388
- profile: bool = False) -> int:
2388
+ profile: bool = False,
2389
+ json_errors: bool = False) -> int:
2389
2390
  """Lex, parse, analyze, and interpret InScript source code."""
2391
+
2392
+ def _emit_error(e):
2393
+ """v2.12.0: emit as JSON or plain text depending on flag."""
2394
+ if json_errors:
2395
+ from inscript_studio_api import error_stream
2396
+ from errors import InScriptError as _ISE
2397
+ if isinstance(e, _ISE):
2398
+ error_stream([e])
2399
+ else:
2400
+ error_stream([{"type": "error", "code": "E0000",
2401
+ "class": type(e).__name__, "message": str(e),
2402
+ "line": 0, "col": 0}])
2403
+ else:
2404
+ print(e, file=sys.stderr)
2390
2405
  # ── 1. Lex ────────────────────────────────────────────────────────────
2391
2406
  try:
2392
2407
  tokens = tokenize(source, filename)
2393
2408
  except LexerError as e:
2394
- print(e, file=sys.stderr)
2395
- return 1
2409
+ _emit_error(e); return 1
2396
2410
 
2397
2411
  # ── 2. Parse ──────────────────────────────────────────────────────────
2398
2412
  try:
2399
2413
  program = parse(source, filename)
2400
2414
  except ParseError as e:
2401
- print(e, file=sys.stderr)
2402
- return 1
2415
+ _emit_error(e); return 1
2403
2416
 
2404
2417
  # ── 3. Analyze (optional) — Phase 3: multi-error + warnings ──────────
2405
2418
  if type_check:
@@ -2442,7 +2455,7 @@ def run_source(source: str, filename: str = "<stdin>",
2442
2455
  interp.run(program)
2443
2456
  return 0
2444
2457
  except InScriptError as e:
2445
- print(e, file=sys.stderr)
2458
+ _emit_error(e)
2446
2459
  return 1
2447
2460
  except KeyboardInterrupt:
2448
2461
  print("\n[InScript] Interrupted.", file=sys.stderr)
@@ -2728,6 +2741,17 @@ Examples:
2728
2741
  help="v2.11.0: Project root for --build (default: current dir)")
2729
2742
  parser.add_argument("--new", metavar="NAME",
2730
2743
  help="v2.11.0: Scaffold a new InScript project: inscript --new mygame")
2744
+ # v2.12.0: studio readiness
2745
+ parser.add_argument("--json-errors", action="store_true",
2746
+ help="v2.12.0: Output errors as newline-delimited JSON for IDE panels")
2747
+ parser.add_argument("--inspect", metavar="DIR", nargs="?", const=".",
2748
+ help="v2.12.0: Project introspection → JSON (scenes, nodes, assets, fns)")
2749
+ parser.add_argument("--check-v3", action="store_true",
2750
+ help="v2.12.0: Run v3.0.0 readiness gates; exits 0 if all pass")
2751
+ parser.add_argument("--studio-bridge", action="store_true",
2752
+ help="v2.12.0: Start Electron bridge JSON-RPC server")
2753
+ parser.add_argument("--bridge-port", type=int, default=8765,
2754
+ help="v2.12.0: Port for --studio-bridge (default: 8765)")
2731
2755
  parser.add_argument("--lsp", action="store_true",
2732
2756
  help="Start the Language Server (requires: pip install pygls)")
2733
2757
  parser.add_argument("--game", action="store_true",
@@ -3012,6 +3036,37 @@ Examples:
3012
3036
  except Exception as _bld:
3013
3037
  print(f"[build] {_bld}", file=sys.stderr); return 1
3014
3038
 
3039
+ # ── v2.12.0 studio readiness ───────────────────────────────────────────
3040
+ if getattr(args, 'check_v3', False):
3041
+ from studio_readiness import run_all_gates
3042
+ base = getattr(args, 'project_dir', '.') or '.'
3043
+ _, all_passed = run_all_gates(base_dir=base)
3044
+ return 0 if all_passed else 1
3045
+
3046
+ if getattr(args, 'inspect', None) is not None:
3047
+ try:
3048
+ from inscript_studio_api import project_inspect
3049
+ import json as _j
3050
+ data = project_inspect(args.inspect)
3051
+ print(_j.dumps(data, indent=2))
3052
+ except Exception as _ie:
3053
+ print(f"[inspect] {_ie}", file=sys.stderr); return 1
3054
+ return 0
3055
+
3056
+ if getattr(args, 'studio_bridge', False):
3057
+ from studio_bridge import StudioBridge
3058
+ import time as _time
3059
+ port = getattr(args, 'bridge_port', 8765)
3060
+ bridge = StudioBridge(port=port)
3061
+ bridge.start()
3062
+ print(f"[studio-bridge] Listening on {bridge.url}")
3063
+ print(f"[studio-bridge] Ctrl+C to stop")
3064
+ try:
3065
+ while True: _time.sleep(1)
3066
+ except KeyboardInterrupt:
3067
+ bridge.stop()
3068
+ return 0
3069
+
3015
3070
  # v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
3016
3071
  if getattr(args, 'no_typecheck', False):
3017
3072
  print(
@@ -3097,7 +3152,8 @@ Examples:
3097
3152
  return run_source(_source, filename=args.file, type_check=type_check,
3098
3153
  no_warn=no_warn, no_warn_unused=no_warn_unused,
3099
3154
  warn_as_error=warn_as_error, strict=strict,
3100
- profile=profile)
3155
+ profile=profile,
3156
+ json_errors=getattr(args, 'json_errors', False))
3101
3157
 
3102
3158
 
3103
3159
  # ─────────────────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.11.0
3
+ Version: 2.13.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -9,6 +9,7 @@ hot_reload.py
9
9
  inscript.py
10
10
  inscript_dap.py
11
11
  inscript_fmt.py
12
+ inscript_studio_api.py
12
13
  inscript_test.py
13
14
  interpreter.py
14
15
  lexer.py
@@ -24,6 +25,8 @@ stdlib_extended.py
24
25
  stdlib_extended_2.py
25
26
  stdlib_game.py
26
27
  stdlib_values.py
28
+ studio_bridge.py
29
+ studio_readiness.py
27
30
  vm.py
28
31
  inscript_lang.egg-info/PKG-INFO
29
32
  inscript_lang.egg-info/SOURCES.txt
@@ -8,6 +8,7 @@ hot_reload
8
8
  inscript
9
9
  inscript_dap
10
10
  inscript_fmt
11
+ inscript_studio_api
11
12
  inscript_test
12
13
  interpreter
13
14
  lexer
@@ -21,4 +22,6 @@ stdlib_extended
21
22
  stdlib_extended_2
22
23
  stdlib_game
23
24
  stdlib_values
25
+ studio_bridge
26
+ studio_readiness
24
27
  vm
@@ -0,0 +1,295 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ inscript_studio_api.py — InScript v2.12.0 Studio Plugin API
4
+ ================================================================
5
+ Stable extension points that InScript Studio (Electron) and third-party
6
+ plugins use to integrate with the runtime.
7
+
8
+ StudioPlugin — base class for plugins; override hook methods
9
+ StudioAPI — registry; emit events to all registered plugins
10
+ project_inspect — static analysis → JSON scene/node/fn/asset list
11
+ error_stream — format errors as newline-delimited JSON for the IDE panel
12
+ """
13
+
14
+ from __future__ import annotations
15
+ import os, re, json
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # Plugin base class
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ class StudioPlugin:
24
+ """
25
+ Base class for Studio plugins. Override any hook you care about.
26
+
27
+ Plugins are registered with the global StudioAPI singleton:
28
+ from inscript_studio_api import get_api
29
+ get_api().register(MyPlugin())
30
+ """
31
+ name: str = "unnamed_plugin"
32
+
33
+ def on_scene_save(self, scene_path: str, tree) -> None:
34
+ """Called when a .inscene file is saved from the editor."""
35
+
36
+ def on_asset_import(self, handle) -> None:
37
+ """Called when an AssetHandle is registered in the AssetRegistry."""
38
+
39
+ def on_error(self, error: dict) -> None:
40
+ """Called when the interpreter raises an InScriptError.
41
+ error is a dict matching InScriptError.to_dict()."""
42
+
43
+ def on_breakpoint(self, file: str, line: int, env: dict) -> None:
44
+ """Called when a breakpoint is hit (DAP integration)."""
45
+
46
+ def on_reload(self, result) -> None:
47
+ """Called after every successful hot reload."""
48
+
49
+ def on_build(self, target: str, output_dir: str, success: bool) -> None:
50
+ """Called after inscript build completes."""
51
+
52
+ def __repr__(self):
53
+ return f"<StudioPlugin {self.name}>"
54
+
55
+
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+ # StudioAPI registry
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ class StudioAPI:
61
+ """
62
+ Central registry for Studio plugins. Emits lifecycle events to all
63
+ registered plugins. Singleton — use get_api() rather than instantiating.
64
+ """
65
+ def __init__(self):
66
+ self._plugins: List[StudioPlugin] = []
67
+
68
+ def register(self, plugin: StudioPlugin) -> "StudioAPI":
69
+ if plugin not in self._plugins:
70
+ self._plugins.append(plugin)
71
+ return self
72
+
73
+ def unregister(self, plugin: StudioPlugin) -> bool:
74
+ if plugin in self._plugins:
75
+ self._plugins.remove(plugin)
76
+ return True
77
+ return False
78
+
79
+ def plugin_count(self) -> int:
80
+ return len(self._plugins)
81
+
82
+ def emit(self, event: str, *args, **kwargs) -> List[Any]:
83
+ """
84
+ Emit an event to all registered plugins.
85
+ Returns list of return values (Nones filtered out).
86
+ """
87
+ results = []
88
+ for plugin in list(self._plugins):
89
+ handler = getattr(plugin, event, None)
90
+ if handler is None:
91
+ continue
92
+ try:
93
+ rv = handler(*args, **kwargs)
94
+ if rv is not None:
95
+ results.append(rv)
96
+ except Exception as e:
97
+ print(f"[StudioAPI] Plugin '{plugin.name}' error in {event}: {e}")
98
+ return results
99
+
100
+ # Convenience emitters matching StudioPlugin hooks
101
+ def emit_scene_save(self, scene_path: str, tree) -> None:
102
+ self.emit("on_scene_save", scene_path, tree)
103
+
104
+ def emit_asset_import(self, handle) -> None:
105
+ self.emit("on_asset_import", handle)
106
+
107
+ def emit_error(self, error: dict) -> None:
108
+ self.emit("on_error", error)
109
+
110
+ def emit_breakpoint(self, file: str, line: int, env: dict) -> None:
111
+ self.emit("on_breakpoint", file, line, env)
112
+
113
+ def emit_reload(self, result) -> None:
114
+ self.emit("on_reload", result)
115
+
116
+ def emit_build(self, target: str, output_dir: str, success: bool) -> None:
117
+ self.emit("on_build", target, output_dir, success)
118
+
119
+ def __repr__(self):
120
+ return f"<StudioAPI plugins={len(self._plugins)}>"
121
+
122
+
123
+ # Module-level singleton
124
+ _GLOBAL_API = StudioAPI()
125
+
126
+ def get_api() -> StudioAPI:
127
+ return _GLOBAL_API
128
+
129
+
130
+ # ─────────────────────────────────────────────────────────────────────────────
131
+ # Project introspection — static analysis without running the interpreter
132
+ # ─────────────────────────────────────────────────────────────────────────────
133
+
134
+ # Patterns for extracting declarations from .ins source (fast, no full parse)
135
+ _RE_NODE = re.compile(r'^\s*node\s+(\w+)\s*\{', re.MULTILINE)
136
+ _RE_SCENE = re.compile(r'^\s*scene\s+(\w+)\s*\{', re.MULTILINE)
137
+ _RE_STRUCT = re.compile(r'^\s*struct\s+(\w+)\s*\{', re.MULTILINE)
138
+ _RE_FN = re.compile(r'^\s*(?:export\s+)?fn\s+(\w+)\s*\(', re.MULTILINE)
139
+ _RE_TEXTURE = re.compile(r'@texture\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
140
+ _RE_SOUND = re.compile(r'@sound\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
141
+ _RE_TILEMAP = re.compile(r'@tilemap\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
142
+ _RE_FONT = re.compile(r'@font\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
143
+ _RE_IMPORT = re.compile(r'^\s*import\s+["\']([^"\']+)["\']', re.MULTILINE)
144
+
145
+
146
+ def _scan_ins_file(file_path: str) -> dict:
147
+ """
148
+ Scan a single .ins file and return a summary dict:
149
+ {file, nodes, scenes, structs, functions, assets, imports}
150
+ """
151
+ try:
152
+ with open(file_path, encoding="utf-8") as f:
153
+ src = f.read()
154
+ except OSError:
155
+ return {}
156
+
157
+ rel = file_path # caller normalises
158
+
159
+ assets = []
160
+ for kind, pat in (("texture", _RE_TEXTURE), ("sound", _RE_SOUND),
161
+ ("tilemap", _RE_TILEMAP), ("font", _RE_FONT)):
162
+ for m in pat.finditer(src):
163
+ assets.append({"type": kind, "path": m.group(1)})
164
+
165
+ return {
166
+ "file": rel,
167
+ "nodes": [m.group(1) for m in _RE_NODE.finditer(src)],
168
+ "scenes": [m.group(1) for m in _RE_SCENE.finditer(src)],
169
+ "structs": [m.group(1) for m in _RE_STRUCT.finditer(src)],
170
+ "functions": [m.group(1) for m in _RE_FN.finditer(src)],
171
+ "assets": assets,
172
+ "imports": [m.group(1) for m in _RE_IMPORT.finditer(src)],
173
+ }
174
+
175
+
176
+ def _scan_inscene_file(file_path: str) -> Optional[dict]:
177
+ """Read a .inscene file and return {file, scene_name, root, nodes}."""
178
+ try:
179
+ with open(file_path, encoding="utf-8") as f:
180
+ content = f.read()
181
+ except OSError:
182
+ return None
183
+
184
+ name_m = re.search(r'^name\s*=\s*"([^"]+)"', content, re.MULTILINE)
185
+ root_m = re.search(r'^root\s*=\s*"([^"]+)"', content, re.MULTILINE)
186
+ nodes = re.findall(r'^type\s*=\s*"([^"]+)"', content, re.MULTILINE)
187
+
188
+ return {
189
+ "file": file_path,
190
+ "scene_name": name_m.group(1) if name_m else "",
191
+ "root": root_m.group(1) if root_m else "",
192
+ "nodes": nodes,
193
+ }
194
+
195
+
196
+ def project_inspect(project_dir: str = ".") -> dict:
197
+ """
198
+ Statically analyse a project directory and return a JSON-serialisable
199
+ summary of all declarations. Used by the Studio scene panel, node
200
+ inspector, and asset browser.
201
+
202
+ Returns:
203
+ {
204
+ "project_dir": str,
205
+ "version": str, # inscript.VERSION
206
+ "sources": [...], # per-file scan results
207
+ "scenes": [...], # merged list of scene names
208
+ "nodes": [...], # merged list of node names
209
+ "structs": [...],
210
+ "functions": [...],
211
+ "assets": [...],
212
+ "scene_files": [...], # .inscene file summaries
213
+ "imports": [...],
214
+ }
215
+ """
216
+ from inscript import VERSION
217
+
218
+ project_dir = os.path.abspath(project_dir)
219
+ sources = []
220
+ scene_files = []
221
+
222
+ all_scenes = []
223
+ all_nodes = []
224
+ all_structs = []
225
+ all_functions = []
226
+ all_assets = []
227
+ all_imports = set()
228
+
229
+ # Walk the project for .ins and .inscene files
230
+ for root, dirs, files in os.walk(project_dir):
231
+ # Skip build output and hidden dirs
232
+ dirs[:] = [d for d in dirs
233
+ if d not in ("build", "__pycache__", "inscript_runtime")
234
+ and not d.startswith(".")]
235
+ for fn in sorted(files):
236
+ full = os.path.join(root, fn)
237
+ rel = os.path.relpath(full, project_dir)
238
+ if fn.endswith(".ins"):
239
+ scan = _scan_ins_file(full)
240
+ scan["file"] = rel
241
+ sources.append(scan)
242
+ all_scenes += [{"name": n, "file": rel} for n in scan["nodes"]]
243
+ all_nodes += [{"name": n, "file": rel} for n in scan["nodes"]]
244
+ all_structs += [{"name": n, "file": rel} for n in scan["structs"]]
245
+ all_functions += [{"name": n, "file": rel} for n in scan["functions"]]
246
+ all_assets += [dict(a, file=rel) for a in scan["assets"]]
247
+ all_imports |= set(scan["imports"])
248
+ elif fn.endswith(".inscene"):
249
+ sf = _scan_inscene_file(full)
250
+ if sf:
251
+ sf["file"] = rel
252
+ scene_files.append(sf)
253
+ all_scenes.append({"name": sf["scene_name"], "file": rel,
254
+ "root": sf["root"]})
255
+
256
+ return {
257
+ "project_dir": project_dir,
258
+ "version": VERSION,
259
+ "sources": sources,
260
+ "scenes": all_scenes,
261
+ "nodes": all_nodes,
262
+ "structs": all_structs,
263
+ "functions": all_functions,
264
+ "assets": all_assets,
265
+ "scene_files": scene_files,
266
+ "imports": sorted(all_imports),
267
+ }
268
+
269
+
270
+ # ─────────────────────────────────────────────────────────────────────────────
271
+ # Error stream — newline-delimited JSON for IDE panels
272
+ # ─────────────────────────────────────────────────────────────────────────────
273
+
274
+ def error_stream(errors: list, file=None) -> None:
275
+ """
276
+ Write errors as newline-delimited JSON to `file` (default: sys.stderr).
277
+ Each line is a self-contained JSON object.
278
+
279
+ Format:
280
+ {"type":"error","code":"E0042","message":"...","line":5,"col":3}
281
+ {"type":"error","code":"E0001","message":"...","line":12,"col":1}
282
+ """
283
+ import sys as _sys
284
+ out = file or _sys.stderr
285
+ for err in errors:
286
+ if hasattr(err, "to_dict"):
287
+ d = err.to_dict()
288
+ elif isinstance(err, dict):
289
+ d = err
290
+ else:
291
+ d = {"type": "error", "code": "E0000",
292
+ "class": type(err).__name__, "message": str(err),
293
+ "line": 0, "col": 0}
294
+ out.write(json.dumps(d, separators=(",", ":")) + "\n")
295
+ out.flush()
@@ -61,7 +61,8 @@ py-modules = [
61
61
  # ── Backends ──────────────────────────────────────────────────────────────
62
62
  "pygame_backend",
63
63
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────────
64
- "scene_tree", "hot_reload", "export_pipeline"
64
+ "scene_tree", "hot_reload", "export_pipeline",
65
+ "studio_bridge", "inscript_studio_api", "studio_readiness"
65
66
  ]
66
67
 
67
68
  [tool.setuptools.package-data]
@@ -56,6 +56,7 @@ setup(
56
56
  "pygame_backend",
57
57
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────
58
58
  "scene_tree", "hot_reload", "export_pipeline",
59
+ "studio_bridge", "inscript_studio_api", "studio_readiness",
59
60
  ],
60
61
  package_data = {"": ["examples/*.ins", "lsp/*.py", "*.md"]},
61
62
  install_requires = [],