haywire-studio 0.0.2__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.
- haywire_studio-0.0.2/.gitignore +205 -0
- haywire_studio-0.0.2/PKG-INFO +9 -0
- haywire_studio-0.0.2/pyproject.toml +41 -0
- haywire_studio-0.0.2/src/haywire_studio/__init__.py +3 -0
- haywire_studio-0.0.2/src/haywire_studio/__main__.py +3 -0
- haywire_studio-0.0.2/src/haywire_studio/app.py +406 -0
- haywire_studio-0.0.2/src/haywire_studio/config.py +121 -0
- haywire_studio-0.0.2/src/haywire_studio/init.py +471 -0
- haywire_studio-0.0.2/src/haywire_studio/py.typed +0 -0
- haywire_studio-0.0.2/src/haywire_studio/share.py +913 -0
- haywire_studio-0.0.2/src/haywire_studio/workspace/__init__.py +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
**/.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
.idea/
|
|
169
|
+
|
|
170
|
+
# Abstra
|
|
171
|
+
# Abstra is an AI-powered process automation framework.
|
|
172
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
173
|
+
# Learn more at https://abstra.io/docs
|
|
174
|
+
.abstra/
|
|
175
|
+
|
|
176
|
+
# Visual Studio Code
|
|
177
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
178
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
179
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
180
|
+
# you could uncomment the following to ignore the enitre vscode folder
|
|
181
|
+
.vscode/
|
|
182
|
+
|
|
183
|
+
# Ruff stuff:
|
|
184
|
+
.ruff_cache/
|
|
185
|
+
|
|
186
|
+
# PyPI configuration file
|
|
187
|
+
.pypirc
|
|
188
|
+
|
|
189
|
+
# Cursor
|
|
190
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
191
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
192
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
193
|
+
.cursorignore
|
|
194
|
+
.cursorindexingignore
|
|
195
|
+
|
|
196
|
+
.DS_Store
|
|
197
|
+
saves/*.*
|
|
198
|
+
|
|
199
|
+
# Dev-mode graphs (local testing only, not part of the repo)
|
|
200
|
+
graphs/*.json
|
|
201
|
+
# MkDocs build output
|
|
202
|
+
site/
|
|
203
|
+
|
|
204
|
+
# Local git worktrees (claude-code workflow)
|
|
205
|
+
.worktrees/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haywire-studio
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Haywire Node Editor Application
|
|
5
|
+
Author: Haywire Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: application,haywire,node-editor,visual-programming
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: haywire-core~=0.0.2
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "haywire-studio"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "Haywire Node Editor Application"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Haywire Team"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["haywire", "node-editor", "visual-programming", "application"]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"haywire-core~=0.0.2",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
haywire = "haywire_studio:main"
|
|
22
|
+
|
|
23
|
+
[tool.uv.sources]
|
|
24
|
+
haywire-core = { workspace = true }
|
|
25
|
+
haybale-core = { workspace = true }
|
|
26
|
+
haybale-studio = { workspace = true }
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/haywire_studio"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist]
|
|
32
|
+
include = [
|
|
33
|
+
"src/haywire_studio/",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
python_version = "3.10"
|
|
38
|
+
|
|
39
|
+
[[tool.mypy.overrides]]
|
|
40
|
+
module = ["toml", "haywire.*"]
|
|
41
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HaywireApp — main application entry point.
|
|
3
|
+
|
|
4
|
+
Manages shared services (libraries) and per-session UI shells.
|
|
5
|
+
Each browser connection gets its own Session and AppShell; all sessions share
|
|
6
|
+
the same library registry.
|
|
7
|
+
|
|
8
|
+
Execution is per-graph: each GraphEntry owns its own Interpreter,
|
|
9
|
+
started/stopped via entry.start_execution() / entry.stop_execution().
|
|
10
|
+
|
|
11
|
+
Haystack lifecycle (open graphs, auto-load on startup) is handled by
|
|
12
|
+
HaystackState, accessed via ctx.app_data[HaystackState].
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from nicegui import ui, app
|
|
22
|
+
|
|
23
|
+
# Core imports
|
|
24
|
+
from haywire.core.graph.editor import Editor
|
|
25
|
+
from haywire.core.graph.base import BaseGraph
|
|
26
|
+
from haywire.core.undo.config import DEVELOPMENT_CONFIG
|
|
27
|
+
from haywire.core.di.config import create_library_system_service
|
|
28
|
+
from haywire.core.di.context import set_workspace_root
|
|
29
|
+
from haywire.core.host import HostStore
|
|
30
|
+
|
|
31
|
+
# UI imports
|
|
32
|
+
from haywire.ui.console_bridge import get_bridge
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from haywire.ui.app.shell import AppShell
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HaywireApp:
|
|
41
|
+
"""Main Haywire application.
|
|
42
|
+
|
|
43
|
+
Constructs shared services (library system, session manager, workspace manager)
|
|
44
|
+
and registers per-session UI shells. Graph/haystack lifecycle is delegated
|
|
45
|
+
to HaystackState (accessed via ctx.app_data[HaystackState]).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, workspace_root: str | None = None):
|
|
49
|
+
self.workspace_root = workspace_root or os.getcwd()
|
|
50
|
+
set_workspace_root(self.workspace_root)
|
|
51
|
+
print(f"Haywire workspace: {self.workspace_root}")
|
|
52
|
+
print("Setting up Haywire application...")
|
|
53
|
+
|
|
54
|
+
self.setup_library_system()
|
|
55
|
+
self.setup_shared_services()
|
|
56
|
+
|
|
57
|
+
self._is_shutting_down = False
|
|
58
|
+
self._shells: dict[str, "AppShell"] = {}
|
|
59
|
+
# Maps NiceGUI client.id → haywire session_id so on_disconnect can
|
|
60
|
+
# resolve which session to tear down without monkey-patching the
|
|
61
|
+
# Client object.
|
|
62
|
+
self._client_to_session: dict[str, str] = {}
|
|
63
|
+
|
|
64
|
+
app.on_disconnect(self.on_disconnect)
|
|
65
|
+
app.on_shutdown(self.on_app_shutdown)
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Lifecycle
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def on_app_shutdown(self):
|
|
72
|
+
"""Clean up all resources on application shutdown."""
|
|
73
|
+
if self._is_shutting_down:
|
|
74
|
+
return
|
|
75
|
+
self._is_shutting_down = True
|
|
76
|
+
print("Application shutdown initiated...")
|
|
77
|
+
|
|
78
|
+
# 1. Clean up all sessions
|
|
79
|
+
print(f" Cleaning up {self.session_manager.session_count} sessions...")
|
|
80
|
+
self.session_manager.cleanup_all()
|
|
81
|
+
|
|
82
|
+
# 2. Clean up console bridge
|
|
83
|
+
try:
|
|
84
|
+
bridge = get_bridge()
|
|
85
|
+
bridge.log_elements.clear()
|
|
86
|
+
bridge.clear_history()
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f" Error cleaning up console bridge: {e}")
|
|
89
|
+
|
|
90
|
+
# 3. Cleanup library system
|
|
91
|
+
try:
|
|
92
|
+
if hasattr(self.library_service, "cleanup"):
|
|
93
|
+
self.library_service.cleanup()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f" Error cleaning up library system: {e}")
|
|
96
|
+
|
|
97
|
+
print("Application shutdown complete")
|
|
98
|
+
|
|
99
|
+
def on_disconnect(self, client):
|
|
100
|
+
"""Handle client disconnect.
|
|
101
|
+
|
|
102
|
+
Shell-upstream model (Q7A): tear down the AppShell first, then
|
|
103
|
+
detach the session. SessionManager.remove_session does only state
|
|
104
|
+
cleanup now — UI cleanup is the shell's responsibility.
|
|
105
|
+
"""
|
|
106
|
+
if self._is_shutting_down:
|
|
107
|
+
return
|
|
108
|
+
session_id = self._client_to_session.pop(client.id, None)
|
|
109
|
+
if not session_id:
|
|
110
|
+
return
|
|
111
|
+
print(f"Client disconnected, cleaning up session {session_id[:8]}")
|
|
112
|
+
|
|
113
|
+
shell = self._shells.pop(session_id, None)
|
|
114
|
+
if shell is not None:
|
|
115
|
+
try:
|
|
116
|
+
shell.cleanup()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f" Error cleaning up shell for session {session_id[:8]}: {e}")
|
|
119
|
+
|
|
120
|
+
self.session_manager.remove_session(session_id)
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Shared services setup
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def setup_library_system(self):
|
|
127
|
+
"""Initialize the library system service (shared across sessions)."""
|
|
128
|
+
self.undo_config = DEVELOPMENT_CONFIG
|
|
129
|
+
|
|
130
|
+
library_paths = []
|
|
131
|
+
workspace_libs = os.path.join(self.workspace_root, "barn")
|
|
132
|
+
if os.path.isdir(workspace_libs):
|
|
133
|
+
library_paths.append(workspace_libs)
|
|
134
|
+
|
|
135
|
+
# HostStore — engine bootstrap persistence the LibraryRegistry uses
|
|
136
|
+
# to remember which libraries the user has disabled. File-backed
|
|
137
|
+
# because this is a real workspace; an embedded / headless host
|
|
138
|
+
# could pass HostStore.in_memory() or omit the argument entirely.
|
|
139
|
+
host_store = HostStore(Path(self.workspace_root) / ".haywire" / "host.toml")
|
|
140
|
+
|
|
141
|
+
# create_library_system_service.initialize() publishes both the
|
|
142
|
+
# service (via set_library_system) and the injector (via
|
|
143
|
+
# set_global_injector) BEFORE the enable phase, so AppState.on_enable
|
|
144
|
+
# hooks can resolve framework services from the ambient context.
|
|
145
|
+
self.library_service = create_library_system_service(
|
|
146
|
+
workspace_root=self.workspace_root,
|
|
147
|
+
library_paths=library_paths if library_paths else None,
|
|
148
|
+
enable_file_watching=True,
|
|
149
|
+
watch_settings=False,
|
|
150
|
+
host_store=host_store,
|
|
151
|
+
)
|
|
152
|
+
print("Library system initialized.")
|
|
153
|
+
|
|
154
|
+
def setup_shared_services(self):
|
|
155
|
+
"""Setup services shared across all sessions."""
|
|
156
|
+
from haywire.core.state import LibraryStateContainer
|
|
157
|
+
from haywire.core.session.session_manager import SessionManager
|
|
158
|
+
|
|
159
|
+
# Registries and factories (from DI)
|
|
160
|
+
self.node_registry = self.library_service.get_node_registry()
|
|
161
|
+
self.node_factory = self.library_service.get_node_factory()
|
|
162
|
+
self.skin_factory = self.library_service.get_skin_factory()
|
|
163
|
+
self.adapter_factory = self.library_service.get_adapter_factory()
|
|
164
|
+
self.panel_registry = self.library_service.get_panel_registry()
|
|
165
|
+
self.library_state_container = self.library_service.injector.get(LibraryStateContainer)
|
|
166
|
+
|
|
167
|
+
# SessionManager comes from the DI container; provide_session_manager()
|
|
168
|
+
# also publishes it via set_session_manager() into the ambient context.
|
|
169
|
+
self.session_manager = self.library_service.injector.get(SessionManager)
|
|
170
|
+
|
|
171
|
+
from haywire.core.session.workspace.manager import WorkspaceManager
|
|
172
|
+
|
|
173
|
+
self.workspace_manager = WorkspaceManager(project_path=Path(self.workspace_root))
|
|
174
|
+
|
|
175
|
+
# LibraryManager is now published by haybale-marketplace as
|
|
176
|
+
# LibraryManagerState. Persisted disabled-state is applied by the
|
|
177
|
+
# library system during create_library_system_service. See ADR-0001.
|
|
178
|
+
|
|
179
|
+
print("Shared services configured successfully.")
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
# Graph factory
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _graph_factory(self, graph_id: str, name: str) -> tuple[BaseGraph, Editor]:
|
|
186
|
+
"""Standard factory producing (BaseGraph, Editor) pairs."""
|
|
187
|
+
g = BaseGraph(graph_id, name)
|
|
188
|
+
e = Editor(g, self.node_factory, undo_config=self.undo_config)
|
|
189
|
+
return g, e
|
|
190
|
+
|
|
191
|
+
def save_workspace(self, shell=None, active_graph_path=None) -> None:
|
|
192
|
+
"""Save workspace snapshot atomically.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
shell: The active AppShell. When provided, collects the current slot
|
|
196
|
+
snapshot from it. When None, re-saves the existing snapshot.
|
|
197
|
+
active_graph_path: Path of the currently active graph (unused here;
|
|
198
|
+
retained for call-site compatibility — callers that need to persist
|
|
199
|
+
haystack state call persistence.dump_haystack before this).
|
|
200
|
+
"""
|
|
201
|
+
snapshot = self.workspace_manager.snapshot.copy()
|
|
202
|
+
if shell is not None:
|
|
203
|
+
slot_data = shell.collect_snapshot()
|
|
204
|
+
snapshot.update(slot_data)
|
|
205
|
+
self.workspace_manager.save(snapshot)
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# UI creation
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def setup_services(self):
|
|
212
|
+
"""Stub kept for compatibility."""
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def create_ui(self):
|
|
216
|
+
"""Register NiceGUI page routes."""
|
|
217
|
+
|
|
218
|
+
@ui.page("/", title="Haywire")
|
|
219
|
+
def main_page():
|
|
220
|
+
from haywire.ui.app.shell import AppShell
|
|
221
|
+
from haywire.ui.editor.registry import EditorTypeRegistry
|
|
222
|
+
from nicegui import context
|
|
223
|
+
|
|
224
|
+
print(f"Creating UI for session: {context.client.id[:8]}")
|
|
225
|
+
|
|
226
|
+
editor_registry = self.library_service.injector.get(EditorTypeRegistry)
|
|
227
|
+
|
|
228
|
+
haywire_session = self.session_manager.create_session(
|
|
229
|
+
project_state=self,
|
|
230
|
+
workspace_manager=self.workspace_manager,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Map this client to its session so on_disconnect can resolve
|
|
234
|
+
# which session to tear down.
|
|
235
|
+
self._client_to_session[context.client.id] = haywire_session.session_id
|
|
236
|
+
|
|
237
|
+
# Set studio theme defaults on context before rendering
|
|
238
|
+
haywire_session.context.active_workbench_theme_key = "core:theme:workbench:haywire-dark"
|
|
239
|
+
haywire_session.context.active_node_theme_key = "core:theme:node:default"
|
|
240
|
+
|
|
241
|
+
app_shell = AppShell(haywire_session, editor_registry=editor_registry)
|
|
242
|
+
self._shells[haywire_session.session_id] = app_shell
|
|
243
|
+
app_shell.render()
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
# Run / cleanup
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def cleanup(self):
|
|
250
|
+
"""Manual cleanup fallback."""
|
|
251
|
+
self.on_app_shutdown()
|
|
252
|
+
|
|
253
|
+
def run(self):
|
|
254
|
+
"""Run the application."""
|
|
255
|
+
print("Starting Haywire...")
|
|
256
|
+
self.create_ui()
|
|
257
|
+
try:
|
|
258
|
+
ui.run(
|
|
259
|
+
port=8082,
|
|
260
|
+
show=True,
|
|
261
|
+
title="Haywire",
|
|
262
|
+
reload=False,
|
|
263
|
+
)
|
|
264
|
+
except KeyboardInterrupt:
|
|
265
|
+
print("\nKeyboard interrupt received")
|
|
266
|
+
finally:
|
|
267
|
+
if not self._is_shutting_down:
|
|
268
|
+
self.cleanup()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# Entry points
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def run_app():
|
|
277
|
+
"""Launch the Haywire application."""
|
|
278
|
+
# logging.getLogger("haywire.ui.editor.graph_canvas_manager").setLevel(logging.DEBUG)
|
|
279
|
+
# use DebugSettings.log_ui instead
|
|
280
|
+
app_instance = HaywireApp()
|
|
281
|
+
app.on_shutdown(app_instance.cleanup)
|
|
282
|
+
app_instance.run()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main():
|
|
286
|
+
"""Main entry point — routes CLI subcommands."""
|
|
287
|
+
import argparse
|
|
288
|
+
|
|
289
|
+
parser = argparse.ArgumentParser(
|
|
290
|
+
prog="haywire",
|
|
291
|
+
description="Haywire visual programming system",
|
|
292
|
+
)
|
|
293
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
294
|
+
|
|
295
|
+
init_parser = subparsers.add_parser("init", help="Create a new haywire project")
|
|
296
|
+
init_parser.add_argument("name", help="Project name")
|
|
297
|
+
init_parser.add_argument(
|
|
298
|
+
"--no-sync",
|
|
299
|
+
action="store_true",
|
|
300
|
+
help="Skip running uv sync after scaffolding",
|
|
301
|
+
)
|
|
302
|
+
init_parser.add_argument(
|
|
303
|
+
"--dev",
|
|
304
|
+
action="store_true",
|
|
305
|
+
help="Use editable local sources from this dev repo instead of PyPI",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
share_parser = subparsers.add_parser(
|
|
309
|
+
"share", help="Generate a marketplace.toml snippet for sharing a library"
|
|
310
|
+
)
|
|
311
|
+
share_parser.add_argument(
|
|
312
|
+
"library_path",
|
|
313
|
+
nargs="?",
|
|
314
|
+
default=None,
|
|
315
|
+
help="Path to the library directory (e.g. libs/haybale-myproject). "
|
|
316
|
+
"Auto-detected if libs/ contains exactly one library.",
|
|
317
|
+
)
|
|
318
|
+
share_parser.add_argument(
|
|
319
|
+
"--save",
|
|
320
|
+
action="store_true",
|
|
321
|
+
help="Aggregate every barn/* library into <repo-root>/marketstall.toml.",
|
|
322
|
+
)
|
|
323
|
+
share_parser.add_argument(
|
|
324
|
+
"--strict",
|
|
325
|
+
action="store_true",
|
|
326
|
+
help="Exit non-zero if any library has dependency drift (declared vs. imported).",
|
|
327
|
+
)
|
|
328
|
+
share_parser.add_argument(
|
|
329
|
+
"--fix",
|
|
330
|
+
action="store_true",
|
|
331
|
+
help="Auto-correct dependency drift by updating pyproject.toml and the "
|
|
332
|
+
"@library decorator before sharing.",
|
|
333
|
+
)
|
|
334
|
+
share_parser.add_argument(
|
|
335
|
+
"--ref",
|
|
336
|
+
type=str,
|
|
337
|
+
default=None,
|
|
338
|
+
help="Specific ref (branch, tag, or SHA) to encode in the share URL.",
|
|
339
|
+
)
|
|
340
|
+
share_parser.add_argument(
|
|
341
|
+
"--tag",
|
|
342
|
+
type=str,
|
|
343
|
+
default=None,
|
|
344
|
+
help="Tag to encode in the share URL. Use 'latest' to resolve to the most recent tag reachable from HEAD.", # noqa: E501
|
|
345
|
+
)
|
|
346
|
+
share_parser.add_argument(
|
|
347
|
+
"--no-update-readme",
|
|
348
|
+
dest="update_readme",
|
|
349
|
+
action="store_false",
|
|
350
|
+
default=True,
|
|
351
|
+
help="Don't rewrite the marketstall:share-url marker block in any README.",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
args = parser.parse_args()
|
|
355
|
+
|
|
356
|
+
if args.command == "init":
|
|
357
|
+
from .init import init_project, _get_dev_repo_root
|
|
358
|
+
|
|
359
|
+
dev_repo = _get_dev_repo_root() if args.dev else None
|
|
360
|
+
init_project(args.name, auto_sync=not args.no_sync, dev_repo=dev_repo)
|
|
361
|
+
elif args.command == "share":
|
|
362
|
+
if args.save:
|
|
363
|
+
from pathlib import Path
|
|
364
|
+
|
|
365
|
+
from .share import DriftError, NoBarnError, share_save_repo
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
result = share_save_repo(
|
|
369
|
+
Path.cwd(),
|
|
370
|
+
strict=args.strict,
|
|
371
|
+
fix=args.fix,
|
|
372
|
+
ref=args.ref,
|
|
373
|
+
tag=args.tag,
|
|
374
|
+
update_readme=args.update_readme,
|
|
375
|
+
)
|
|
376
|
+
print(f"✓ Wrote {result.out_path}")
|
|
377
|
+
if result.share_url is not None:
|
|
378
|
+
print(f"✓ Share this URL:\n {result.share_url}")
|
|
379
|
+
elif result.warning is not None:
|
|
380
|
+
print(f"⚠ {result.warning}")
|
|
381
|
+
except NoBarnError as exc:
|
|
382
|
+
print(f"Error: {exc}")
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
except DriftError as exc:
|
|
385
|
+
print(str(exc), file=sys.stderr)
|
|
386
|
+
sys.exit(1)
|
|
387
|
+
elif args.library_path is not None:
|
|
388
|
+
from .share import share_library
|
|
389
|
+
|
|
390
|
+
share_library(args.library_path, strict=args.strict, fix=args.fix)
|
|
391
|
+
else:
|
|
392
|
+
from pathlib import Path
|
|
393
|
+
|
|
394
|
+
from .share import derive_share_url_only
|
|
395
|
+
|
|
396
|
+
result = derive_share_url_only(Path.cwd(), ref=args.ref, tag=args.tag)
|
|
397
|
+
if result.share_url is not None:
|
|
398
|
+
print(f"✓ Share this URL:\n {result.share_url}")
|
|
399
|
+
elif result.warning is not None:
|
|
400
|
+
print(f"⚠ {result.warning}")
|
|
401
|
+
else:
|
|
402
|
+
run_app()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ in {"__main__", "__mp_main__"}:
|
|
406
|
+
main()
|