katalyst-plugin-bard 0.1.1a1__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.
- katalyst_plugin_bard/__init__.py +5 -0
- katalyst_plugin_bard/build.py +224 -0
- katalyst_plugin_bard/cli.py +234 -0
- katalyst_plugin_bard/contrib/build.mjs +65 -0
- katalyst_plugin_bard/contrib/shared.json +10 -0
- katalyst_plugin_bard/contributions.py +176 -0
- katalyst_plugin_bard/plugin.py +44 -0
- katalyst_plugin_bard/py.typed +0 -0
- katalyst_plugin_bard/schemas/bard.manifest.schema.json +178 -0
- katalyst_plugin_bard/serve.py +626 -0
- katalyst_plugin_bard/web_dist/desktop/main.cjs +8369 -0
- katalyst_plugin_bard/web_dist/desktop/package.json +6 -0
- katalyst_plugin_bard/web_dist/server/index.cjs +43202 -0
- katalyst_plugin_bard/web_dist/server/index.cjs.map +7 -0
- katalyst_plugin_bard/web_dist/web/assets/Chat-CU3UoVyk.js +42 -0
- katalyst_plugin_bard/web_dist/web/assets/Chat-CpKl2eGN.css +1 -0
- katalyst_plugin_bard/web_dist/web/assets/DataTable-D45zW-iy.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/GraphCanvas-LOIKzHVV.js +331 -0
- katalyst_plugin_bard/web_dist/web/assets/KindsView-DYW_khzh.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/NodeDetail-DUwFKFxk.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/NodesView-R6LjlOlL.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/SchemaGraphPanel-CxW6ysWN.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/SettingsView-E-EGyvjS.js +130 -0
- katalyst_plugin_bard/web_dist/web/assets/TooltipIconButton-DxN4d6_s.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/TreeNav-BnDYMAw4.js +9373 -0
- katalyst_plugin_bard/web_dist/web/assets/WingsView-DvKWpWka.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/_commonjsHelpers-CqkleIqs.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/client-BVWFISPM.js +25 -0
- katalyst_plugin_bard/web_dist/web/assets/empty-Ct-3knBG.js +2 -0
- katalyst_plugin_bard/web_dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/index-Bn5I7isx.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/index-C2oMlmGY.css +1 -0
- katalyst_plugin_bard/web_dist/web/assets/index-CbYvlL0N.js +9 -0
- katalyst_plugin_bard/web_dist/web/assets/index-DZ8IcxKl.js +9 -0
- katalyst_plugin_bard/web_dist/web/assets/index-DuLQ-QSA.js +2 -0
- katalyst_plugin_bard/web_dist/web/assets/index-Iz84zNIC.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/jsx-runtime-l_A6JZ6M.js +9 -0
- katalyst_plugin_bard/web_dist/web/assets/nunito-sans-cyrillic-ext-wght-normal-7w74d1lj.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/nunito-sans-cyrillic-wght-normal-B9hoeQUC.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/nunito-sans-latin-ext-wght-normal-DMEAsnHD.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/nunito-sans-latin-wght-normal-BWQ3gi2K.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/nunito-sans-vietnamese-wght-normal-BhFYcaJr.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/shared-katalyst-bard-host-hVOU1c3P.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/shared-react-CBKIYMuS.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/shared-react-dom-EXphnHyn.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/shared-react-dom-client-BZbsmXA9.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/shared-react-jsx-runtime--VkjFgMQ.js +1 -0
- katalyst_plugin_bard/web_dist/web/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- katalyst_plugin_bard/web_dist/web/index.html +33 -0
- katalyst_plugin_bard-0.1.1a1.dist-info/METADATA +84 -0
- katalyst_plugin_bard-0.1.1a1.dist-info/RECORD +59 -0
- katalyst_plugin_bard-0.1.1a1.dist-info/WHEEL +4 -0
- katalyst_plugin_bard-0.1.1a1.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Build a contribution: validate its source manifest, bundle it, write the built manifest.
|
|
2
|
+
|
|
3
|
+
``kata tax bard build-contribution <source> --out <dir>`` is the one build a
|
|
4
|
+
contribution author needs. It turns a source directory (``bard.manifest.json``
|
|
5
|
+
naming ``.ts``/``.tsx`` entries, importing only ``@katalyst/bard-host``) into a
|
|
6
|
+
directory Bard loads at runtime: one ES module per view or slot, its stylesheet,
|
|
7
|
+
and a built manifest naming them. Ship that directory in a Python package and
|
|
8
|
+
declare it under the ``katalyst_bard.contributions`` entry point
|
|
9
|
+
(:mod:`katalyst_plugin_bard.contributions`).
|
|
10
|
+
|
|
11
|
+
The bundling itself is ``contrib/build.mjs`` (esbuild, from the contribution's own
|
|
12
|
+
``node_modules``). React, react-dom and the host contract are not bundled: the
|
|
13
|
+
host provides them at runtime (``contrib/shared.json``), so there is one React.
|
|
14
|
+
|
|
15
|
+
Validation happens here, before Node runs, so a bad manifest fails with the
|
|
16
|
+
reason rather than as a bundler error far from its cause.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import copy
|
|
22
|
+
import json
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from katalyst_plugin_bard.contributions import MANIFEST
|
|
31
|
+
|
|
32
|
+
#: The bundling script and the shared-module list it reads, shipped in this package.
|
|
33
|
+
BUILD_SCRIPT = Path(__file__).parent / "contrib" / "build.mjs"
|
|
34
|
+
|
|
35
|
+
#: Predicates the runtime cannot honour yet. Rejected at build time so the failure is
|
|
36
|
+
#: a build error rather than a fragment that renders nowhere. See
|
|
37
|
+
#: katalyst-taxonomy#196 — the REST API does not expose ``spec.extends``.
|
|
38
|
+
UNSUPPORTED_PREDICATES = ("attachedKind",)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class BuiltEntry:
|
|
43
|
+
id: str
|
|
44
|
+
module: str
|
|
45
|
+
styles: tuple[str, ...]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class BuildResult:
|
|
50
|
+
name: str
|
|
51
|
+
out: Path
|
|
52
|
+
entries: tuple[BuiltEntry, ...]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#: ``(script, payload, cwd) -> stdout``. Raises RuntimeError on failure.
|
|
56
|
+
RunNode = Callable[[Path, dict[str, Any], Path], str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def default_schema_path() -> Path:
|
|
60
|
+
"""The schema shipped inside the wheel, falling back to a repo checkout."""
|
|
61
|
+
packaged = Path(__file__).parent / "schemas" / "bard.manifest.schema.json"
|
|
62
|
+
if packaged.is_file():
|
|
63
|
+
return packaged
|
|
64
|
+
# src/katalyst_plugin_bard/build.py -> plugin root is parents[2].
|
|
65
|
+
return Path(__file__).parents[2] / "schemas" / "bard.manifest.schema.json"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_schema(schema_path: Path | None = None) -> dict[str, Any]:
|
|
69
|
+
"""Load the contribution manifest schema."""
|
|
70
|
+
return json.loads((schema_path or default_schema_path()).read_text())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _schema_errors(doc: Any, schema: dict[str, Any]) -> list[str]:
|
|
74
|
+
"""Validation messages for a document, as plain strings.
|
|
75
|
+
|
|
76
|
+
`jsonschema` ships no type stubs, so this is the single place the untyped
|
|
77
|
+
boundary is crossed — keeping the ignore localised instead of scattered
|
|
78
|
+
through the validation logic.
|
|
79
|
+
"""
|
|
80
|
+
from jsonschema import Draft202012Validator
|
|
81
|
+
|
|
82
|
+
validator = Draft202012Validator(schema) # pyright: ignore[reportUnknownMemberType]
|
|
83
|
+
messages: list[str] = []
|
|
84
|
+
for error in validator.iter_errors(doc): # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
85
|
+
path = "/".join(str(p) for p in error.absolute_path) or "(root)" # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
|
|
86
|
+
messages.append(f"{path}: {error.message}") # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
|
|
87
|
+
return messages
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_source(source: Path, schema: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
|
91
|
+
"""Read and validate a source manifest. Returns the document and every problem."""
|
|
92
|
+
manifest = source / MANIFEST
|
|
93
|
+
if not manifest.is_file():
|
|
94
|
+
return {}, [f"no {MANIFEST} in {source}"]
|
|
95
|
+
try:
|
|
96
|
+
doc: dict[str, Any] = json.loads(manifest.read_text())
|
|
97
|
+
except json.JSONDecodeError as exc:
|
|
98
|
+
return {}, [f"invalid JSON: {exc}"]
|
|
99
|
+
|
|
100
|
+
errors = _schema_errors(doc, schema)
|
|
101
|
+
if errors:
|
|
102
|
+
return doc, errors
|
|
103
|
+
|
|
104
|
+
problems: list[str] = []
|
|
105
|
+
name = doc["metadata"]["name"]
|
|
106
|
+
if name != source.resolve().name:
|
|
107
|
+
problems.append(
|
|
108
|
+
f"metadata.name '{name}' does not match directory '{source.resolve().name}'"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
spec: dict[str, Any] = doc.get("spec", {})
|
|
112
|
+
if "hostRange" not in spec:
|
|
113
|
+
# Bard skips a contribution it cannot check, so building one would only defer
|
|
114
|
+
# the failure to whoever installs it.
|
|
115
|
+
problems.append("spec.hostRange is required: the host versions this contribution supports")
|
|
116
|
+
|
|
117
|
+
entries: list[dict[str, Any]] = [*spec.get("views", ()), *spec.get("slots", ())]
|
|
118
|
+
if not entries:
|
|
119
|
+
problems.append("declares no views or slots — it would contribute nothing")
|
|
120
|
+
|
|
121
|
+
seen: set[str] = set()
|
|
122
|
+
for entry in entries:
|
|
123
|
+
if entry["id"] in seen:
|
|
124
|
+
problems.append(f"id '{entry['id']}' is declared twice")
|
|
125
|
+
seen.add(entry["id"])
|
|
126
|
+
if not (source / entry["entry"]).is_file():
|
|
127
|
+
problems.append(f"entry '{entry['entry']}' (id '{entry['id']}') does not exist")
|
|
128
|
+
|
|
129
|
+
for view in spec.get("views", ()):
|
|
130
|
+
if view.get("placement") != "footer" and "section" not in view and "section" not in spec:
|
|
131
|
+
# Bard refuses a contributed main view with no subsection to list it under.
|
|
132
|
+
problems.append(
|
|
133
|
+
f"view '{view['id']}' declares no section: set \"section\" on the view, or "
|
|
134
|
+
"on spec for all of them — the subsection it is listed under in your "
|
|
135
|
+
"package's sidebar heading"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for slot in spec.get("slots", ()):
|
|
139
|
+
applies = slot.get("appliesWhen", {})
|
|
140
|
+
for predicate in UNSUPPORTED_PREDICATES:
|
|
141
|
+
if predicate in applies:
|
|
142
|
+
problems.append(
|
|
143
|
+
f"slot '{slot['id']}' uses appliesWhen.{predicate}, which the runtime "
|
|
144
|
+
"cannot honour yet (the taxonomy API does not expose spec.extends — "
|
|
145
|
+
"see katalyst-taxonomy#196)"
|
|
146
|
+
)
|
|
147
|
+
return doc, problems
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _default_run_node(script: Path, payload: dict[str, Any], cwd: Path) -> str:
|
|
151
|
+
node = shutil.which("node")
|
|
152
|
+
if node is None:
|
|
153
|
+
raise RuntimeError("node is required to build a contribution. Install Node.js 20 or newer.")
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
[node, str(script), json.dumps(payload)],
|
|
156
|
+
cwd=cwd,
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
check=False,
|
|
160
|
+
)
|
|
161
|
+
if result.returncode != 0:
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
f"bundling failed (node exit {result.returncode}):\n{result.stderr.strip()}"
|
|
164
|
+
)
|
|
165
|
+
return result.stdout
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _prepare_out(out: Path) -> None:
|
|
169
|
+
"""Empty *out* for a fresh build, refusing a directory that is not a previous one.
|
|
170
|
+
|
|
171
|
+
The whole directory is replaced so a module renamed in source does not linger in
|
|
172
|
+
the shipped build; a directory with other content is refused rather than wiped.
|
|
173
|
+
"""
|
|
174
|
+
if out.exists():
|
|
175
|
+
if not out.is_dir():
|
|
176
|
+
raise RuntimeError(f"--out {out} is not a directory")
|
|
177
|
+
if any(out.iterdir()) and not (out / MANIFEST).is_file():
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
f"--out {out} is not empty and holds no {MANIFEST}; refusing to replace it"
|
|
180
|
+
)
|
|
181
|
+
shutil.rmtree(out)
|
|
182
|
+
out.mkdir(parents=True)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_contribution(
|
|
186
|
+
source: Path,
|
|
187
|
+
out: Path,
|
|
188
|
+
*,
|
|
189
|
+
schema: dict[str, Any] | None = None,
|
|
190
|
+
run_node: RunNode = _default_run_node,
|
|
191
|
+
) -> BuildResult:
|
|
192
|
+
"""Validate *source*, bundle it into *out*, and write the built manifest there."""
|
|
193
|
+
doc, problems = validate_source(source, schema if schema is not None else load_schema())
|
|
194
|
+
if problems:
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f"{source / MANIFEST} is not buildable:\n" + "\n".join(f" - {p}" for p in problems)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
spec: dict[str, Any] = doc["spec"]
|
|
200
|
+
entries = {e["id"]: e["entry"] for e in (*spec.get("views", ()), *spec.get("slots", ()))}
|
|
201
|
+
_prepare_out(out)
|
|
202
|
+
stdout = run_node(
|
|
203
|
+
BUILD_SCRIPT,
|
|
204
|
+
{"root": str(source.resolve()), "out": str(out.resolve()), "entries": entries},
|
|
205
|
+
source.resolve(),
|
|
206
|
+
)
|
|
207
|
+
bundled: dict[str, dict[str, Any]] = json.loads(stdout)
|
|
208
|
+
|
|
209
|
+
missing = sorted(set(entries) - set(bundled))
|
|
210
|
+
if missing:
|
|
211
|
+
raise RuntimeError(f"bundling produced no module for: {', '.join(missing)}")
|
|
212
|
+
|
|
213
|
+
built = copy.deepcopy(doc)
|
|
214
|
+
results: list[BuiltEntry] = []
|
|
215
|
+
for key in ("views", "slots"):
|
|
216
|
+
for entry in built["spec"].get(key, ()):
|
|
217
|
+
output = bundled[entry["id"]]
|
|
218
|
+
entry["entry"] = output["module"]
|
|
219
|
+
styles = tuple(output.get("styles", ()))
|
|
220
|
+
if styles:
|
|
221
|
+
entry["styles"] = list(styles)
|
|
222
|
+
results.append(BuiltEntry(entry["id"], output["module"], styles))
|
|
223
|
+
(out / MANIFEST).write_text(json.dumps(built, indent=2) + "\n")
|
|
224
|
+
return BuildResult(doc["metadata"]["name"], out, tuple(results))
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""CLI contribution — `kata tax bard <command>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from katalyst_plugin_bard.contributions import NO_CONTRIBUTIONS_ENV
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Output(Protocol):
|
|
14
|
+
def echo(self, message: str) -> None: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CliContextLike(Protocol):
|
|
18
|
+
"""The slice of the CLI context bard needs.
|
|
19
|
+
|
|
20
|
+
Declared structurally rather than importing ``CliContext``: the concrete
|
|
21
|
+
class lives in the CLI package, and depending on the shape rather than the
|
|
22
|
+
type keeps this plugin loadable without it while staying type-checked.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def output(self) -> _Output: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_group() -> click.Group:
|
|
30
|
+
"""Factory called by CLI discovery to register the bard command group."""
|
|
31
|
+
|
|
32
|
+
@click.group("bard")
|
|
33
|
+
def bard() -> None:
|
|
34
|
+
"""Bard — a schema-driven web UI for the taxonomy."""
|
|
35
|
+
|
|
36
|
+
@bard.command("serve")
|
|
37
|
+
@click.option("--port", "ui_port", type=int, default=5273, help="Port for the bard UI.")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--api-port",
|
|
40
|
+
type=int,
|
|
41
|
+
default=None,
|
|
42
|
+
help=(
|
|
43
|
+
"Port for the local REST API when Bard starts one; starting fails if it is in "
|
|
44
|
+
"use. An API already running for your config file is reused on its own port. "
|
|
45
|
+
"Default: a free port."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--api-url",
|
|
50
|
+
default=None,
|
|
51
|
+
help=(
|
|
52
|
+
"Use an existing taxonomy API instead of starting one locally. "
|
|
53
|
+
"Bard reads all data over HTTP, so this can be any reachable server; "
|
|
54
|
+
"its token is read from KATALYST_API_TOKEN."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
@click.option("--no-open", is_flag=True, help="Do not open a browser.")
|
|
58
|
+
@click.option(
|
|
59
|
+
"--no-contributions",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
envvar=NO_CONTRIBUTIONS_ENV,
|
|
62
|
+
help=(
|
|
63
|
+
"Plain Bard: load none of the views installed packages contribute, only "
|
|
64
|
+
f"Bard's own. Also set by {NO_CONTRIBUTIONS_ENV}=1."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
@click.option("--dev", is_flag=True, help="Print dev-server instructions instead of serving.")
|
|
68
|
+
@click.pass_obj
|
|
69
|
+
def serve(
|
|
70
|
+
ctx: CliContextLike,
|
|
71
|
+
ui_port: int,
|
|
72
|
+
api_port: int | None,
|
|
73
|
+
api_url: str | None,
|
|
74
|
+
no_open: bool,
|
|
75
|
+
no_contributions: bool,
|
|
76
|
+
dev: bool,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Serve the bard UI, ensuring a taxonomy API is reachable.
|
|
79
|
+
|
|
80
|
+
Runs from any directory: the API starts from the user config file, and every
|
|
81
|
+
repository is a filesystem backend declared there. With KATALYST_CONFIG_WRITE=1
|
|
82
|
+
the local API accepts configuration writes (a running one that does not is
|
|
83
|
+
restarted). Requires Node.js 20+ and built assets (`just build` in plugins/bard).
|
|
84
|
+
"""
|
|
85
|
+
# Deferred: CLI discovery imports this module on every `kata` invocation,
|
|
86
|
+
# so the heavy import stays inside the command body.
|
|
87
|
+
from katalyst_plugin_bard.serve import BardConfig, guard_teardown, run_bard
|
|
88
|
+
|
|
89
|
+
if dev:
|
|
90
|
+
ctx.output.echo("Dev mode — run the Node server and Vite dev server manually:")
|
|
91
|
+
ctx.output.echo(" cd plugins/bard/web && npm install && npm run dev")
|
|
92
|
+
ctx.output.echo("Set TAXONOMY_API_URL if your API is not on http://127.0.0.1:8000")
|
|
93
|
+
raise SystemExit(0)
|
|
94
|
+
|
|
95
|
+
config = BardConfig(
|
|
96
|
+
ui_port=ui_port,
|
|
97
|
+
api_port=api_port,
|
|
98
|
+
open_browser=not no_open,
|
|
99
|
+
taxonomy_api_url=api_url,
|
|
100
|
+
contributions=not no_contributions,
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
url, handle = run_bard(config, echo=ctx.output.echo)
|
|
104
|
+
except RuntimeError as exc:
|
|
105
|
+
ctx.output.echo(f"Error: {exc}")
|
|
106
|
+
raise SystemExit(1) from exc
|
|
107
|
+
|
|
108
|
+
guard_teardown(handle)
|
|
109
|
+
ctx.output.echo(f"Bard serving at {url}")
|
|
110
|
+
ctx.output.echo("Press Ctrl+C to stop.")
|
|
111
|
+
try:
|
|
112
|
+
code = handle.wait()
|
|
113
|
+
except KeyboardInterrupt:
|
|
114
|
+
handle.terminate()
|
|
115
|
+
code = 0
|
|
116
|
+
raise SystemExit(code)
|
|
117
|
+
|
|
118
|
+
@bard.command("desktop")
|
|
119
|
+
@click.option(
|
|
120
|
+
"--api-url",
|
|
121
|
+
default=None,
|
|
122
|
+
help=(
|
|
123
|
+
"Use an existing taxonomy API instead of starting one locally; its token is "
|
|
124
|
+
"read from KATALYST_API_TOKEN. Settings can save only if that API was started "
|
|
125
|
+
"with KATALYST_CONFIG_WRITE=1."
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--api-port",
|
|
130
|
+
type=int,
|
|
131
|
+
default=None,
|
|
132
|
+
help=(
|
|
133
|
+
"Port for the local REST API when Bard starts one; starting fails if it is in "
|
|
134
|
+
"use. An API already running for your config file is reused on its own port. "
|
|
135
|
+
"Default: a free port."
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
@click.option(
|
|
139
|
+
"--electron",
|
|
140
|
+
"electron",
|
|
141
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
142
|
+
default=None,
|
|
143
|
+
help=(
|
|
144
|
+
"Electron executable to use. Default: KATALYST_ELECTRON, else the pinned "
|
|
145
|
+
"version, fetched into the Katalyst cache with npm on first run."
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
@click.option(
|
|
149
|
+
"--no-contributions",
|
|
150
|
+
is_flag=True,
|
|
151
|
+
envvar=NO_CONTRIBUTIONS_ENV,
|
|
152
|
+
help=(
|
|
153
|
+
"Plain Bard: load none of the views installed packages contribute, only "
|
|
154
|
+
f"Bard's own. Also set by {NO_CONTRIBUTIONS_ENV}=1."
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
@click.pass_obj
|
|
158
|
+
def desktop(
|
|
159
|
+
ctx: CliContextLike,
|
|
160
|
+
api_url: str | None,
|
|
161
|
+
api_port: int | None,
|
|
162
|
+
electron: Path | None,
|
|
163
|
+
no_contributions: bool,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Open bard in a desktop window, with settings editable.
|
|
166
|
+
|
|
167
|
+
The desktop shell serves the UI over a custom protocol rather than a
|
|
168
|
+
local HTTP server, so there is no port to bind and nothing to collide
|
|
169
|
+
with. It starts the taxonomy API for your config file with configuration
|
|
170
|
+
writes enabled. With --api-url it starts nothing, and saving settings
|
|
171
|
+
needs that API to have been started with KATALYST_CONFIG_WRITE=1.
|
|
172
|
+
Requires Node.js 20+ (npm fetches Electron on first run).
|
|
173
|
+
"""
|
|
174
|
+
from katalyst_plugin_bard.serve import BardConfig, guard_teardown, run_desktop
|
|
175
|
+
|
|
176
|
+
config = BardConfig(
|
|
177
|
+
ui_port=0, # unused: the desktop shell binds no port
|
|
178
|
+
api_port=api_port,
|
|
179
|
+
open_browser=False,
|
|
180
|
+
taxonomy_api_url=api_url,
|
|
181
|
+
contributions=not no_contributions,
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
handle = run_desktop(config, electron=electron, echo=ctx.output.echo)
|
|
185
|
+
except RuntimeError as exc:
|
|
186
|
+
ctx.output.echo(f"Error: {exc}")
|
|
187
|
+
raise SystemExit(1) from exc
|
|
188
|
+
|
|
189
|
+
guard_teardown(handle)
|
|
190
|
+
ctx.output.echo("Bard desktop running. Close the window to exit.")
|
|
191
|
+
try:
|
|
192
|
+
code = handle.wait()
|
|
193
|
+
except KeyboardInterrupt:
|
|
194
|
+
handle.terminate()
|
|
195
|
+
code = 0
|
|
196
|
+
raise SystemExit(code)
|
|
197
|
+
|
|
198
|
+
@bard.command("build-contribution")
|
|
199
|
+
@click.argument("source", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
200
|
+
@click.option(
|
|
201
|
+
"--out",
|
|
202
|
+
required=True,
|
|
203
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
204
|
+
help="Directory to write the built contribution to. Replaced on every build.",
|
|
205
|
+
)
|
|
206
|
+
@click.pass_obj
|
|
207
|
+
def build_contribution_cmd(ctx: CliContextLike, source: Path, out: Path) -> None:
|
|
208
|
+
"""Build a contribution for Bard to load at runtime.
|
|
209
|
+
|
|
210
|
+
SOURCE holds a bard.manifest.json naming .ts/.tsx entries that import only
|
|
211
|
+
@katalyst/bard-host. Writes one ES module per view or slot, its stylesheet and
|
|
212
|
+
the built manifest to --out; ship that directory in a Python package under the
|
|
213
|
+
`katalyst_bard.contributions` entry point. Requires Node.js 20+ and esbuild
|
|
214
|
+
installed for SOURCE (a devDependency).
|
|
215
|
+
"""
|
|
216
|
+
from katalyst_plugin_bard.build import build_contribution
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
result = build_contribution(source, out)
|
|
220
|
+
except RuntimeError as exc:
|
|
221
|
+
ctx.output.echo(f"Error: {exc}")
|
|
222
|
+
raise SystemExit(1) from exc
|
|
223
|
+
|
|
224
|
+
for entry in result.entries:
|
|
225
|
+
styles = f" + {', '.join(entry.styles)}" if entry.styles else ""
|
|
226
|
+
ctx.output.echo(f" {entry.id}: {entry.module}{styles}")
|
|
227
|
+
ctx.output.echo(f"Built {result.name} into {result.out} ({len(result.entries)} module(s))")
|
|
228
|
+
|
|
229
|
+
_ = (
|
|
230
|
+
serve,
|
|
231
|
+
desktop,
|
|
232
|
+
build_contribution_cmd,
|
|
233
|
+
) # registered by decorators; silence unused warnings
|
|
234
|
+
return bard
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Bundle a Bard contribution's entries into browser ES modules.
|
|
2
|
+
//
|
|
3
|
+
// Run by `kata tax bard build-contribution`, which validates the manifest first and
|
|
4
|
+
// writes the built manifest after; this script only bundles. Input is one JSON
|
|
5
|
+
// argument: { root, out, entries: { <id>: <entry path relative to root> } }. Output,
|
|
6
|
+
// on stdout, is { <id>: { module, styles } } with paths relative to `out`.
|
|
7
|
+
//
|
|
8
|
+
// The modules in shared.json are not bundled: they stay bare imports, which the
|
|
9
|
+
// host's import map resolves to its own React and host contract. Any bundler that
|
|
10
|
+
// leaves exactly those external produces an equivalent module.
|
|
11
|
+
//
|
|
12
|
+
// esbuild comes from the contribution's own node_modules (a devDependency), so the
|
|
13
|
+
// host does not have to ship a bundler. ESBUILD_BINARY_PATH is honoured, as always.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { join, relative, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const shared = JSON.parse(readFileSync(new URL("./shared.json", import.meta.url), "utf8"));
|
|
20
|
+
const { root, out, entries } = JSON.parse(process.argv[2] ?? "{}");
|
|
21
|
+
|
|
22
|
+
function loadEsbuild(from) {
|
|
23
|
+
try {
|
|
24
|
+
return createRequire(join(from, "package.json"))("esbuild");
|
|
25
|
+
} catch {
|
|
26
|
+
process.stderr.write(
|
|
27
|
+
`esbuild is not installed for ${from}. Add it as a devDependency ` +
|
|
28
|
+
"(npm install --save-dev esbuild) and run this again.\n",
|
|
29
|
+
);
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const esbuild = loadEsbuild(resolve(root));
|
|
35
|
+
const absOut = resolve(out);
|
|
36
|
+
const result = await esbuild.build({
|
|
37
|
+
absWorkingDir: resolve(root),
|
|
38
|
+
entryPoints: Object.fromEntries(Object.entries(entries).map(([id, entry]) => [id, resolve(root, entry)])),
|
|
39
|
+
outdir: absOut,
|
|
40
|
+
entryNames: "[name]",
|
|
41
|
+
assetNames: "assets/[name]-[hash]",
|
|
42
|
+
bundle: true,
|
|
43
|
+
format: "esm",
|
|
44
|
+
platform: "browser",
|
|
45
|
+
target: "es2022",
|
|
46
|
+
jsx: "automatic",
|
|
47
|
+
minify: true,
|
|
48
|
+
legalComments: "none",
|
|
49
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
50
|
+
loader: { ".svg": "file", ".png": "file", ".woff2": "file" },
|
|
51
|
+
metafile: true,
|
|
52
|
+
logLevel: "warning",
|
|
53
|
+
// Left as bare imports: the host's import map resolves them to its own instances.
|
|
54
|
+
external: shared.modules,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const built = {};
|
|
58
|
+
for (const [file, output] of Object.entries(result.metafile.outputs)) {
|
|
59
|
+
if (output.entryPoint === undefined || !file.endsWith(".js")) continue;
|
|
60
|
+
const id = Object.keys(entries).find((key) => resolve(root, entries[key]) === resolve(root, output.entryPoint));
|
|
61
|
+
if (id === undefined) continue;
|
|
62
|
+
const rel = (p) => `./${relative(absOut, resolve(root, p)).split("\\").join("/")}`;
|
|
63
|
+
built[id] = { module: rel(file), styles: output.cssBundle ? [rel(output.cssBundle)] : [] };
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(JSON.stringify(built));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Modules a contribution imports from the Bard host at runtime instead of bundling. The host's build serves each as a module re-exporting its own instance and maps the name to it in an import map (web/vite/shared-modules.ts), so every contribution renders with the host's React. build-contribution marks exactly these external.",
|
|
3
|
+
"modules": [
|
|
4
|
+
"@katalyst/bard-host",
|
|
5
|
+
"react",
|
|
6
|
+
"react-dom",
|
|
7
|
+
"react-dom/client",
|
|
8
|
+
"react/jsx-runtime"
|
|
9
|
+
]
|
|
10
|
+
}
|