@omochi-ui/cli 0.1.2 → 0.2.0
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.
- package/README.md +47 -261
- package/bin/omochi-ui.cjs +34 -0
- package/package.json +23 -27
- package/bin/omochi-ui.js +0 -2
- package/dist/catalog-codegen.d.ts +0 -9
- package/dist/catalog-codegen.js +0 -185
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -454
- package/dist/help.d.ts +0 -3
- package/dist/help.js +0 -187
- package/dist/hub/dashboards.d.ts +0 -17
- package/dist/hub/dashboards.js +0 -230
- package/dist/hub/gateway-supervisor.d.ts +0 -19
- package/dist/hub/gateway-supervisor.js +0 -75
- package/dist/hub/hub.d.ts +0 -41
- package/dist/hub/hub.js +0 -353
- package/dist/hub/ssh-gateway.d.ts +0 -16
- package/dist/hub/ssh-gateway.js +0 -110
- package/dist/hub/static-ui.d.ts +0 -2
- package/dist/hub/static-ui.js +0 -30
- package/dist/stdio-websocket-bridge.d.ts +0 -17
- package/dist/stdio-websocket-bridge.js +0 -78
- package/dist/up.d.ts +0 -21
- package/dist/up.js +0 -229
package/README.md
CHANGED
|
@@ -1,279 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Omochi native services
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The archive contains omochi-ui and omochi-utils for the target in manifest.json.
|
|
4
|
+
The production dashboard is embedded. Neither binary requires Bun, Node.js,
|
|
5
|
+
Python or Rust at runtime. Applications launched by them need their own runtime.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
root using `bun run omochi-ui`; the binary is provided by `@omochi-ui/cli`; the TypeScript SDK is
|
|
7
|
-
`@omochi-ui/sdk`.
|
|
7
|
+
Verify the archive against its .sha256 file, extract it, and install both binaries:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
bun run omochi-ui help generate catalog
|
|
13
|
-
```
|
|
9
|
+
./install.sh # $HOME/.local/bin
|
|
10
|
+
./install.sh /your/preferred/bin # optional destination
|
|
11
|
+
omochi-ui hub
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
The default dashboard is http://127.0.0.1:5555. Keep this loopback default.
|
|
14
|
+
Use omochi-ui up omochi.toml for compositions, and omochi-utils --help for utility
|
|
15
|
+
modes. dev bridges application stdio to the separate developer preview frontend;
|
|
16
|
+
the Vite developer preview is not included in this archive.
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Pairing
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
| --- | --- | --- |
|
|
23
|
-
| Start a project composition | `up [FILE]` | Yes; `--open` opens it automatically |
|
|
24
|
-
| Develop a single app | `dev -- <command> [args...]` | Start the Vite preview separately |
|
|
25
|
-
| Compose dashboards from running apps | `hub` | Yes |
|
|
26
|
-
| Publish this machine's running apps | `gateway` | No; connect a hub |
|
|
27
|
-
| Show or name this machine | `id` | No |
|
|
28
|
-
| Pair with a TCP gateway | `pair HOST:PORT` | No |
|
|
29
|
-
| Inspect or remove paired peers | `trust list`, `trust forget <nodeId>` | No |
|
|
30
|
-
| Generate SDK catalog types | `generate catalog` | No |
|
|
20
|
+
On the gateway machine:
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
omochi-ui gateway --remote --pair --name workstation
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
Import its invitation on the hub PC:
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```
|
|
26
|
+
omochi-ui pair --name workstation
|
|
27
|
+
# Paste the invitation on stdin, then Ctrl+D.
|
|
28
|
+
omochi-ui hub
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
without stopping the others. Utility shorthand supports `button`, `watch`,
|
|
46
|
-
`process`, `tail`, and `http`. Stdio-owned `stream` and `mcp` processes stay
|
|
47
|
-
external; this lets an agent connect and disconnect independently of the preset.
|
|
30
|
+
Possession of an invitation grants access. It expires after 180 seconds and admits
|
|
31
|
+
one hub; keep it private. Registered gateways connect automatically without IP
|
|
32
|
+
input. --offline disables public discovery and relays while retaining LAN mDNS
|
|
33
|
+
and direct connections.
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
Online development uses n0 public discovery and relays. For a project relay, use
|
|
36
|
+
the same --relay https://your-relay.example option on remote gateway, pair and hub
|
|
37
|
+
commands. This archive does not provision a relay. The source repository includes host
|
|
38
|
+
configuration and monitoring templates in deploy/relay.
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
status reports registered gateway connection attempts and failures from the running
|
|
41
|
+
hub (use --port if the hub has a non-default port). It does not initialize state.
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
trust list shows peers. trust forget NAME_OR_ENDPOINT_ID waits for live processes
|
|
44
|
+
to destroy revoked connections. An acknowledgement failure remains an error even
|
|
45
|
+
though the trust removal is already durable.
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
name = "Local development"
|
|
47
|
+
## Breaking upgrade
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
Stop old gateways/hubs first. SDK APIs and manifests are unchanged; gateway wire
|
|
50
|
+
protocol and NodeIds are new. Pair devices again and recreate saved dashboards.
|
|
51
|
+
Native state lives at $XDG_CONFIG_HOME/omochi/rust-v1 or
|
|
52
|
+
$HOME/.config/omochi/rust-v1. Legacy state is left untouched.
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
Linux binaries use their build runner's libc; choose an OS/architecture-compatible
|
|
55
|
+
archive and libc, or build from source. This script does not codesign or notarize
|
|
56
|
+
macOS binaries.
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
title = "Application"
|
|
73
|
-
service = "app"
|
|
74
|
-
```
|
|
58
|
+
## Registry distributions
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| --- | --- | --- |
|
|
83
|
-
| `--host HOST` | Hub bind address | `127.0.0.1` |
|
|
84
|
-
| `--port PORT` | Hub port; `0` chooses an available port | `5555` |
|
|
85
|
-
| `--allow-origin ORIGIN` | Add an exact browser origin; repeatable | Loopback origins only |
|
|
86
|
-
| `--open` | Open the selected preset in the desktop browser | Off |
|
|
87
|
-
|
|
88
|
-
## dev
|
|
89
|
-
|
|
90
|
-
Launch a child application and bridge its JSONL stdin/stdout to WebSocket.
|
|
91
|
-
|
|
92
|
-
```sh
|
|
93
|
-
# Terminal 1: browser preview
|
|
94
|
-
bun run --filter @omochi-ui/dev-server dev
|
|
95
|
-
# Terminal 2: app + bridge
|
|
96
|
-
bun run omochi-ui dev -- uv run --project examples/python examples/python/app.py
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Open `http://127.0.0.1:5173/`. The preview's default `/ws` proxy reaches the bridge.
|
|
100
|
-
All arguments after `--` belong to the child, including any `--help` flag.
|
|
101
|
-
Application logging must go to stderr; stdout is reserved for A2UI JSONL.
|
|
102
|
-
|
|
103
|
-
| Option | Meaning | Default |
|
|
104
|
-
| --- | --- | --- |
|
|
105
|
-
| `--host HOST` | Bridge bind address | `127.0.0.1` |
|
|
106
|
-
| `--port PORT` | Bridge port; `0` chooses an available port | `5555` |
|
|
107
|
-
|
|
108
|
-
The bridge serves WebSocket at `/ws`; it does not serve an HTML page. Startup
|
|
109
|
-
prints a preview URL with `?ws=` for its actual address. The preview must already
|
|
110
|
-
be running; use its printed port if Vite selected one other than 5173.
|
|
111
|
-
All clients share application state, and late clients receive a current snapshot.
|
|
112
|
-
The bridge has no authentication or TLS; keep the loopback default.
|
|
113
|
-
|
|
114
|
-
## hub
|
|
115
|
-
|
|
116
|
-
Serve the dashboard UI, discover available surfaces, and route browser actions.
|
|
117
|
-
Apps must already be running with `UnixSocketServerTransport`. The bundled
|
|
118
|
-
examples select that transport when `OMOCHI_SERVICE` is set.
|
|
119
|
-
|
|
120
|
-
```sh
|
|
121
|
-
# Terminal 1
|
|
122
|
-
OMOCHI_SERVICE=python-example uv run --project examples/python examples/python/app.py
|
|
123
|
-
# Terminal 2
|
|
124
|
-
bun run omochi-ui hub
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Open `http://127.0.0.1:5555/`. The hub serves its own UI and automatically starts
|
|
128
|
-
a local gateway when none is listening. It reuses an existing gateway and does
|
|
129
|
-
not stop a gateway it did not start.
|
|
130
|
-
|
|
131
|
-
| Option | Meaning | Default |
|
|
132
|
-
| --- | --- | --- |
|
|
133
|
-
| `--host HOST` | HTTP/WebSocket bind address | `127.0.0.1` |
|
|
134
|
-
| `--port PORT` | HTTP/WebSocket port; `0` chooses an available port | `5555` |
|
|
135
|
-
| `--gateway TARGET` | Unix socket path or `<nodeId>@HOST:PORT`; repeatable | Local gateway |
|
|
136
|
-
| `--ssh HOST` | SSH hostname, alias, or `user@host`; one target | Off |
|
|
137
|
-
| `--ssh-socket PATH` | Remote gateway socket override; requires `--ssh` | Resolved through SSH |
|
|
138
|
-
| `--no-auto-gateway` | Do not start a missing local gateway | Auto-start enabled |
|
|
139
|
-
| `--discover-lan` | Also look for paired gateways via mDNS | Off |
|
|
140
|
-
| `--allow-origin ORIGIN` | Add an exact allowed browser origin; repeatable | Loopback origins only |
|
|
141
|
-
|
|
142
|
-
Explicit `--gateway` or `--ssh` targets replace the default local gateway. Add
|
|
143
|
-
`--gateway /path/to/local/gateway.sock` to include it too. Explicit targets can
|
|
144
|
-
be combined with `--discover-lan`. A TCP target needs its paired node ID; a Unix
|
|
145
|
-
socket or SSH target does not need pairing.
|
|
146
|
-
|
|
147
|
-
```sh
|
|
148
|
-
# Remote machine: gateway and apps must already be running
|
|
149
|
-
bun run omochi-ui gateway
|
|
150
|
-
# Local machine: SSH authenticates and forwards the remote gateway socket
|
|
151
|
-
bun run omochi-ui hub --ssh user@remote
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
The hub has no authentication or TLS on its browser-facing listener. Keep its
|
|
155
|
-
loopback binding. See [SSH setup](../../docs/discovery.md#connecting-through-ssh)
|
|
156
|
-
for requirements and cleanup behavior.
|
|
157
|
-
|
|
158
|
-
## gateway
|
|
159
|
-
|
|
160
|
-
Discover the current user's apps and publish their surfaces. A gateway does not
|
|
161
|
-
launch apps or serve the dashboard UI. It normally listens only on a Unix socket.
|
|
162
|
-
|
|
163
|
-
```sh
|
|
164
|
-
bun run omochi-ui gateway
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
| Option | Meaning | Default |
|
|
168
|
-
| --- | --- | --- |
|
|
169
|
-
| `--root DIR` | Directory containing service manifests and sockets | `$XDG_RUNTIME_DIR/omochi` |
|
|
170
|
-
| `--socket PATH` | Gateway Unix socket | `<root>/gateway.sock` |
|
|
171
|
-
| `--node-name NAME` | Persist this machine's display name | Existing name or hostname |
|
|
172
|
-
| `--listen HOST:PORT` | Enable the TLS TCP listener | Off |
|
|
173
|
-
| `--advertise` | Advertise over mDNS; requires a TCP listener | Off |
|
|
174
|
-
| `--pair` | Open a one-time pairing window; requires a TCP listener | Off |
|
|
175
|
-
| `--lan` | `--listen 0.0.0.0:5556 --advertise` | Off |
|
|
176
|
-
|
|
177
|
-
If `XDG_RUNTIME_DIR` is unset, the registry uses
|
|
178
|
-
`<system temporary directory>/omochi-<uid>/omochi`. Apps and gateways must use the
|
|
179
|
-
same runtime directory. See [Service discovery](../../docs/discovery.md) for the
|
|
180
|
-
manifest format and SDK transport examples.
|
|
181
|
-
|
|
182
|
-
Network reachability, advertisement, and trust are separate. `--listen` enables
|
|
183
|
-
the paired TLS listener; mDNS only advertises its location. Neither allows an
|
|
184
|
-
unpaired hub to read application data.
|
|
185
|
-
|
|
186
|
-
## pair
|
|
187
|
-
|
|
188
|
-
Pair once before connecting a hub to a gateway over TCP. SSH does not require this.
|
|
189
|
-
|
|
190
|
-
```sh
|
|
191
|
-
# Machine running the apps: open a TCP listener and pairing window
|
|
192
|
-
bun run omochi-ui gateway --lan --pair
|
|
193
|
-
# Machine running the hub: enter the code printed by the gateway
|
|
194
|
-
bun run omochi-ui pair 192.168.1.42:42317 # use the random pairing port printed above
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
`pair HOST:PORT` reads the code from stdin. `--code CODE` supplies it explicitly;
|
|
198
|
-
stdin keeps it out of shell history. The window lasts three minutes, permits
|
|
199
|
-
five exchange attempts, and closes after one successful pairing. Its dedicated HTTPS
|
|
200
|
-
listener uses a random port and exposes no application API. Normal connections use
|
|
201
|
-
mutual TLS with pinned device certificates; legacy PSK peers must pair again.
|
|
202
|
-
|
|
203
|
-
After success, run the printed `hub --gateway <nodeId>@HOST:PORT` command, prefixed
|
|
204
|
-
with `bun run` in a source checkout. Alternatively, use `hub --discover-lan`
|
|
205
|
-
when the gateway advertises on the same local network. mDNS generally does not
|
|
206
|
-
cross VLANs or VPNs.
|
|
207
|
-
|
|
208
|
-
## id and trust
|
|
209
|
-
|
|
210
|
-
```sh
|
|
211
|
-
bun run omochi-ui id
|
|
212
|
-
bun run omochi-ui id --node-name workstation
|
|
213
|
-
bun run omochi-ui trust list
|
|
214
|
-
bun run omochi-ui trust forget NODE_ID
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
`id` creates a persistent identity on first use and prints its node ID and name.
|
|
218
|
-
Changing the name preserves the ID. `trust list` prints peer IDs, roles, names,
|
|
219
|
-
and pairing times. `trust forget` removes the local peer entry and waits for running local processes
|
|
220
|
-
to close affected connections before reporting success; it returns code
|
|
221
|
-
1 when the peer does not exist. Pair again to establish trust after removal.
|
|
222
|
-
|
|
223
|
-
## generate catalog
|
|
224
|
-
|
|
225
|
-
Generate typed bindings from a local A2UI v0.9 catalog for any subset of SDKs.
|
|
226
|
-
Create output directories before running the command.
|
|
227
|
-
|
|
228
|
-
```sh
|
|
229
|
-
mkdir -p generated
|
|
230
|
-
bun run omochi-ui generate catalog \
|
|
231
|
-
--catalog ./catalog.json \
|
|
232
|
-
--name ProductCatalog \
|
|
233
|
-
--typescript ./generated/product-catalog.ts \
|
|
234
|
-
--python ./generated/product_catalog.py \
|
|
235
|
-
--rust ./generated/product_catalog.rs
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
| Option | Meaning |
|
|
239
|
-
| --- | --- |
|
|
240
|
-
| `--catalog FILE` | Required catalog JSON input |
|
|
241
|
-
| `--name NAME` | Type name; defaults to catalog title or `GeneratedCatalog` |
|
|
242
|
-
| `--typescript FILE` | TypeScript output |
|
|
243
|
-
| `--python FILE` | Python output |
|
|
244
|
-
| `--rust FILE` | Rust output |
|
|
245
|
-
| `--check` | Check outputs without writing; fail if missing or stale |
|
|
246
|
-
|
|
247
|
-
At least one output flag is required. Generation resolves local schemas only;
|
|
248
|
-
it does not fetch schemas over the network. Reuse the same invocation with
|
|
249
|
-
`--check` in CI.
|
|
250
|
-
|
|
251
|
-
## Ports and storage
|
|
252
|
-
|
|
253
|
-
| Resource | Default |
|
|
254
|
-
| --- | --- |
|
|
255
|
-
| Vite browser preview | `http://127.0.0.1:5173/` |
|
|
256
|
-
| Development bridge | `ws://127.0.0.1:5555/ws` |
|
|
257
|
-
| Hub UI / A2UI | `http://127.0.0.1:5555/` / `ws://127.0.0.1:5555/ws` |
|
|
258
|
-
| Gateway Unix socket | `$XDG_RUNTIME_DIR/omochi/gateway.sock` (fallback above) |
|
|
259
|
-
| Gateway TCP with `--lan` | `0.0.0.0:5556` (paired TLS) |
|
|
260
|
-
| Node identity and peers | `$XDG_CONFIG_HOME/omochi/{node,peers}.json` |
|
|
261
|
-
| Saved dashboards | `$XDG_CONFIG_HOME/omochi/dashboards.json` |
|
|
262
|
-
|
|
263
|
-
`XDG_CONFIG_HOME` defaults to `~/.config`. Runtime directories and configuration
|
|
264
|
-
are per-user; identity and trust persist across restarts. Dashboard files store
|
|
265
|
-
names, surface references, and layouts, not app state.
|
|
266
|
-
|
|
267
|
-
## Troubleshooting
|
|
268
|
-
|
|
269
|
-
| Symptom | What to check |
|
|
270
|
-
| --- | --- |
|
|
271
|
-
| `dev` starts but there is no page | Start `bun run --filter @omochi-ui/dev-server dev` and open its URL, not port 5555 |
|
|
272
|
-
| Preview says disconnected | Start the bridge; with a custom port, use the printed `?ws=` URL |
|
|
273
|
-
| Address already in use | `dev` and `hub` share default port 5555; stop one or set `--port 5557` |
|
|
274
|
-
| Vite uses a port other than 5173 | Use Vite's printed URL, adding the bridge's `?ws=` parameter if needed |
|
|
275
|
-
| Hub has no apps | Run an app with `OMOCHI_SERVICE` set and the same user/runtime directory |
|
|
276
|
-
| Hub loses the local apps when using SSH | Explicit targets replace the local default; add the local socket with `--gateway` |
|
|
277
|
-
| Pairing/advertising requires a listener | Use `gateway --lan --pair` or provide `--listen HOST:PORT` |
|
|
278
|
-
| Remote gateway is not found | Check that it is running; use SSH or an explicit paired TCP target when multicast is unavailable |
|
|
279
|
-
| Command or build output is missing | Run `bun install` and `bun run build` from the repository root |
|
|
60
|
+
Native releases also prepare npm `@omochi-ui/cli` and `@omochi-ui/utils` under
|
|
61
|
+
the existing package names, using exact-version platform binary dependencies.
|
|
62
|
+
The npm launcher needs Node.js 22+; the native archives above do not. Published
|
|
63
|
+
Cargo sources can be installed with `cargo install omochi-cli --locked` and
|
|
64
|
+
`cargo install omochi-utils --locked`. The existing `omochi-ui` crate remains the SDK.
|
|
65
|
+
See docs/native-releasing.md in the repository for release setup and migration.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const manifest = require('../package.json');
|
|
6
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
7
|
+
const dependency = manifest.nativePlatforms[platform];
|
|
8
|
+
function fail(message) { console.error(`[${manifest.name}] ${message}`); process.exit(1); }
|
|
9
|
+
if (!dependency) fail(`Unsupported platform ${platform}. Use a supported Linux/glibc or macOS build.`);
|
|
10
|
+
if (process.platform === 'linux' && !process.report?.getReport().header.glibcVersionRuntime) {
|
|
11
|
+
fail('The prebuilt Linux binary requires glibc. On musl, build with cargo install instead.');
|
|
12
|
+
}
|
|
13
|
+
let binary;
|
|
14
|
+
try {
|
|
15
|
+
const installed = require.resolve(`${dependency}/package.json`);
|
|
16
|
+
const native = require(installed);
|
|
17
|
+
if (native.version !== manifest.version) fail(`Binary version mismatch: expected ${manifest.version}, found ${native.version}. Reinstall the package.`);
|
|
18
|
+
binary = path.join(path.dirname(installed), 'bin', manifest.nativeBinary);
|
|
19
|
+
} catch {
|
|
20
|
+
fail(`Missing ${dependency}@${manifest.version}. Reinstall with optional dependencies enabled (npm install --include=optional).`);
|
|
21
|
+
}
|
|
22
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
23
|
+
const handlers = new Map();
|
|
24
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
25
|
+
const handler = () => child.kill(signal);
|
|
26
|
+
handlers.set(signal, handler);
|
|
27
|
+
process.on(signal, handler);
|
|
28
|
+
}
|
|
29
|
+
child.once('error', error => fail(`Cannot start ${binary}: ${error.message}`));
|
|
30
|
+
child.once('exit', (code, signal) => {
|
|
31
|
+
for (const [name, handler] of handlers) process.removeListener(name, handler);
|
|
32
|
+
if (signal) process.kill(process.pid, signal);
|
|
33
|
+
else process.exitCode = code ?? 1;
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omochi-ui/cli",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Native Omochi cli",
|
|
5
|
+
"files": [
|
|
6
|
+
"bin"
|
|
7
|
+
],
|
|
7
8
|
"engines": {
|
|
8
|
-
"
|
|
9
|
+
"node": ">=22"
|
|
9
10
|
},
|
|
10
|
-
"version": "0.1.2",
|
|
11
|
-
"description": "CLI tools and stdio/WebSocket bridge for omochi-ui",
|
|
12
|
-
"license": "Apache-2.0",
|
|
13
|
-
"type": "module",
|
|
14
11
|
"bin": {
|
|
15
|
-
"omochi-ui": "./bin/omochi-ui.
|
|
12
|
+
"omochi-ui": "./bin/omochi-ui.cjs"
|
|
16
13
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"@omochi-ui/
|
|
26
|
-
"@omochi-ui/
|
|
27
|
-
"
|
|
28
|
-
"
|
|
14
|
+
"nativeBinary": "omochi-ui",
|
|
15
|
+
"nativePlatforms": {
|
|
16
|
+
"linux-x64": "@omochi-ui/native-linux-x64",
|
|
17
|
+
"linux-arm64": "@omochi-ui/native-linux-arm64",
|
|
18
|
+
"darwin-x64": "@omochi-ui/native-darwin-x64",
|
|
19
|
+
"darwin-arm64": "@omochi-ui/native-darwin-arm64"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"@omochi-ui/native-linux-x64": "0.2.0",
|
|
23
|
+
"@omochi-ui/native-linux-arm64": "0.2.0",
|
|
24
|
+
"@omochi-ui/native-darwin-x64": "0.2.0",
|
|
25
|
+
"@omochi-ui/native-darwin-arm64": "0.2.0"
|
|
29
26
|
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
30
28
|
"repository": {
|
|
31
29
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/omochi-ui/omochi.git"
|
|
33
|
-
"directory": "packages/cli"
|
|
30
|
+
"url": "https://github.com/omochi-ui/omochi.git"
|
|
34
31
|
},
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"url": "https://github.com/omochi-ui/omochi/issues"
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
38
34
|
}
|
|
39
35
|
}
|
package/bin/omochi-ui.js
DELETED
package/dist/catalog-codegen.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
4
|
-
const identifier = (value) => value.replace(/[^A-Za-z0-9]+(.)/g, (_, next) => next.toUpperCase()).replace(/^[^A-Za-z_]+/, '');
|
|
5
|
-
const snake = (value) => value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Za-z0-9]+/g, '_').toLowerCase();
|
|
6
|
-
function referenceType(ref, language) {
|
|
7
|
-
const name = ref.split('/').at(-1);
|
|
8
|
-
const maps = {
|
|
9
|
-
ts: { ComponentId: 'string', ChildList: 'string[] | { componentId: string; path: string }', DataBinding: 'DataBinding', FunctionCall: 'FunctionCall', DynamicValue: 'DynamicValue', DynamicString: 'DynamicString', DynamicNumber: 'DynamicNumber', DynamicBoolean: 'DynamicBoolean', DynamicStringList: 'DynamicStringList', AccessibilityAttributes: 'Accessibility', CheckRule: 'CheckRule', Action: 'WireAction' },
|
|
10
|
-
py: { ComponentId: 'str', ChildList: 'list[str] | dict[str, str]', DataBinding: 'DataBinding', FunctionCall: 'FunctionCall', DynamicValue: 'Any', DynamicString: 'DynamicString', DynamicNumber: 'DynamicNumber', DynamicBoolean: 'DynamicBoolean', DynamicStringList: 'DynamicStringList', AccessibilityAttributes: 'dict[str, DynamicString]', CheckRule: 'dict[str, Any]', Action: 'dict[str, Any]' },
|
|
11
|
-
rs: { ComponentId: 'String', ChildList: 'Value', DataBinding: 'Value', FunctionCall: 'Value', DynamicValue: 'Value', DynamicString: 'Value', DynamicNumber: 'Value', DynamicBoolean: 'Value', DynamicStringList: 'Value', AccessibilityAttributes: 'Value', CheckRule: 'Value', Action: 'Value' },
|
|
12
|
-
};
|
|
13
|
-
const value = maps[language][name];
|
|
14
|
-
if (!value)
|
|
15
|
-
throw new Error(`unsupported schema reference: ${ref}`);
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
function schemaType(schema, language) {
|
|
19
|
-
if (typeof schema.$ref === 'string')
|
|
20
|
-
return referenceType(schema.$ref, language);
|
|
21
|
-
if (Array.isArray(schema.allOf)) {
|
|
22
|
-
const values = schema.allOf.filter((item) => !item.if && !item.then).map((item) => schemaType(item, language));
|
|
23
|
-
if (values.length === 1)
|
|
24
|
-
return values[0];
|
|
25
|
-
return language === 'rs' ? 'Value' : values.join(' & ');
|
|
26
|
-
}
|
|
27
|
-
const union = (schema.anyOf ?? schema.oneOf);
|
|
28
|
-
if (union) {
|
|
29
|
-
const values = union.map((item) => schemaType(item, language));
|
|
30
|
-
return language === 'rs' ? 'Value' : values.join(' | ');
|
|
31
|
-
}
|
|
32
|
-
if (Array.isArray(schema.enum)) {
|
|
33
|
-
if (language === 'ts')
|
|
34
|
-
return schema.enum.map((value) => JSON.stringify(value)).join(' | ');
|
|
35
|
-
if (language === 'py')
|
|
36
|
-
return `Literal[${schema.enum.map((value) => JSON.stringify(value)).join(', ')}]`;
|
|
37
|
-
return 'String';
|
|
38
|
-
}
|
|
39
|
-
if (schema.type === 'string')
|
|
40
|
-
return language === 'ts' ? 'string' : language === 'py' ? 'str' : 'String';
|
|
41
|
-
if (schema.type === 'number' || schema.type === 'integer')
|
|
42
|
-
return language === 'ts' ? 'number' : language === 'py' ? 'float' : 'f64';
|
|
43
|
-
if (schema.type === 'boolean')
|
|
44
|
-
return language === 'ts' ? 'boolean' : language === 'py' ? 'bool' : 'bool';
|
|
45
|
-
if (schema.type === 'array') {
|
|
46
|
-
const item = schemaType(schema.items ?? {}, language);
|
|
47
|
-
return language === 'ts' ? `${item}[]` : language === 'py' ? `list[${item}]` : `Vec<${item}>`;
|
|
48
|
-
}
|
|
49
|
-
if (schema.type === 'object' && schema.properties) {
|
|
50
|
-
const entries = Object.entries(schema.properties);
|
|
51
|
-
if (language === 'ts')
|
|
52
|
-
return `{ ${entries.map(([key, value]) => `${JSON.stringify(key)}: ${schemaType(value, language)}`).join('; ')} }`;
|
|
53
|
-
return language === 'py' ? 'dict[str, Any]' : 'Value';
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`unsupported schema fragment: ${JSON.stringify(schema)}`);
|
|
56
|
-
}
|
|
57
|
-
function pointer(document, fragment) {
|
|
58
|
-
if (!fragment || fragment === '#')
|
|
59
|
-
return document;
|
|
60
|
-
if (!fragment.startsWith('#/'))
|
|
61
|
-
throw new Error(`unsupported JSON Schema fragment: ${fragment}`);
|
|
62
|
-
return fragment.slice(2).split('/').reduce((value, part) => {
|
|
63
|
-
if (!value || typeof value !== 'object')
|
|
64
|
-
throw new Error(`unresolved JSON Schema fragment: ${fragment}`);
|
|
65
|
-
return value[part.replaceAll('~1', '/').replaceAll('~0', '~')];
|
|
66
|
-
}, document);
|
|
67
|
-
}
|
|
68
|
-
async function resolveLocalRefs(value, document, base, stack = new Set()) {
|
|
69
|
-
if (Array.isArray(value))
|
|
70
|
-
return Promise.all(value.map((item) => resolveLocalRefs(item, document, base, stack)));
|
|
71
|
-
if (!value || typeof value !== 'object')
|
|
72
|
-
return value;
|
|
73
|
-
const object = value;
|
|
74
|
-
if (typeof object.$ref === 'string' && !object.$ref.startsWith('http:') && !object.$ref.startsWith('https:')) {
|
|
75
|
-
const [filename, rawFragment = ''] = object.$ref.split('#', 2);
|
|
76
|
-
const fragment = `#${rawFragment}`;
|
|
77
|
-
const referenceKey = `${base}:${object.$ref}`;
|
|
78
|
-
if (stack.has(referenceKey))
|
|
79
|
-
throw new Error(`circular schema reference: ${object.$ref}`);
|
|
80
|
-
const nextStack = new Set(stack).add(referenceKey);
|
|
81
|
-
if (filename) {
|
|
82
|
-
const path = resolve(base, filename);
|
|
83
|
-
const external = JSON.parse(await readFile(path, 'utf8'));
|
|
84
|
-
const resolved = pointer(external, fragment);
|
|
85
|
-
return resolveLocalRefs({ ...resolved, ...Object.fromEntries(Object.entries(object).filter(([key]) => key !== '$ref')) }, external, dirname(path), nextStack);
|
|
86
|
-
}
|
|
87
|
-
const resolved = pointer(document, fragment);
|
|
88
|
-
return resolveLocalRefs({ ...resolved, ...Object.fromEntries(Object.entries(object).filter(([key]) => key !== '$ref')) }, document, base, nextStack);
|
|
89
|
-
}
|
|
90
|
-
return Object.fromEntries(await Promise.all(Object.entries(object).map(async ([key, item]) => [key, await resolveLocalRefs(item, document, base, stack)])));
|
|
91
|
-
}
|
|
92
|
-
function normalize(raw, requestedName, sourceHash) {
|
|
93
|
-
const catalog = JSON.parse(raw);
|
|
94
|
-
if (!catalog.components || typeof catalog.components !== 'object')
|
|
95
|
-
throw new Error('catalog must contain a components object');
|
|
96
|
-
const title = requestedName ?? (typeof catalog.title === 'string' ? catalog.title : 'GeneratedCatalog');
|
|
97
|
-
const name = identifier(title);
|
|
98
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
99
|
-
throw new Error(`catalog name is not a valid identifier: ${title}`);
|
|
100
|
-
const components = Object.entries(catalog.components).map(([componentName, schema]) => {
|
|
101
|
-
const layers = Array.isArray(schema.allOf) ? schema.allOf : [schema];
|
|
102
|
-
const properties = new Map();
|
|
103
|
-
const required = new Set();
|
|
104
|
-
for (const layer of layers) {
|
|
105
|
-
if (typeof layer.$ref === 'string') {
|
|
106
|
-
if (layer.$ref.endsWith('/ComponentCommon'))
|
|
107
|
-
properties.set('accessibility', { $ref: 'https://a2ui.org/specification/v0_9/common_types.json#/$defs/AccessibilityAttributes' });
|
|
108
|
-
else if (layer.$ref.endsWith('/Checkable')) {
|
|
109
|
-
properties.set('checks', { type: 'array', items: { $ref: 'https://a2ui.org/specification/v0_9/common_types.json#/$defs/CheckRule' } });
|
|
110
|
-
properties.set('isValid', { type: 'boolean' });
|
|
111
|
-
properties.set('validationErrors', { type: 'array', items: { type: 'string' } });
|
|
112
|
-
}
|
|
113
|
-
else if (layer.$ref === '#/$defs/CatalogComponentCommon')
|
|
114
|
-
properties.set('weight', { type: 'number' });
|
|
115
|
-
else
|
|
116
|
-
throw new Error(`unsupported component mixin: ${layer.$ref}`);
|
|
117
|
-
}
|
|
118
|
-
for (const [key, value] of Object.entries(layer.properties ?? {}))
|
|
119
|
-
if (key !== 'component' && key !== 'id')
|
|
120
|
-
properties.set(key, value);
|
|
121
|
-
for (const key of layer.required ?? [])
|
|
122
|
-
if (key !== 'component' && key !== 'id')
|
|
123
|
-
required.add(key);
|
|
124
|
-
}
|
|
125
|
-
return { name: componentName, properties: [...properties].map(([propertyName, propertySchema]) => ({ name: propertyName, type: JSON.stringify(propertySchema), required: required.has(propertyName) })) };
|
|
126
|
-
});
|
|
127
|
-
return { id: String(catalog.catalogId ?? catalog.$id ?? ''), name, hash: sourceHash ?? createHash('sha256').update(raw).digest('hex'), components };
|
|
128
|
-
}
|
|
129
|
-
const propertyType = (property, language) => schemaType(JSON.parse(property.type), language);
|
|
130
|
-
const header = (model, comment) => `${comment} Generated by omochi-ui from catalog ${model.id}\n${comment} Source SHA-256: ${model.hash}\n`;
|
|
131
|
-
function typescript(model) {
|
|
132
|
-
const body = model.components.map((component) => `export interface ${component.name}Props {\n${component.properties.map((property) => ` ${JSON.stringify(property.name)}${property.required ? '' : '?'}: ${propertyType(property, 'ts')}`).join('\n')}\n}`).join('\n\n');
|
|
133
|
-
const map = model.components.map((component) => ` ${JSON.stringify(component.name)}: ${component.name}Props`).join('\n');
|
|
134
|
-
const knownTypes = ['Accessibility', 'CheckRule', 'DataBinding', 'DynamicBoolean', 'DynamicNumber', 'DynamicString', 'DynamicStringList', 'DynamicValue', 'FunctionCall', 'WireAction'];
|
|
135
|
-
const imports = ['CatalogDefinition', ...knownTypes.filter((name) => body.includes(name))];
|
|
136
|
-
return `${header(model, '//')}import type { ${imports.join(', ')} } from '@omochi-ui/sdk'\n\n${body}\n\nexport interface ${model.name}ComponentMap {\n${map}\n}\n\nexport const ${model.name.toUpperCase()}: CatalogDefinition<${model.name}ComponentMap> = { id: ${JSON.stringify(model.id)}, name: ${JSON.stringify(model.name)} }\n`;
|
|
137
|
-
}
|
|
138
|
-
function python(model) {
|
|
139
|
-
const body = model.components.map((component) => `class ${component.name}Props(TypedDict, total=False):\n${component.properties.map((property) => ` ${property.name}: ${property.required ? 'Required[' : ''}${propertyType(property, 'py')}${property.required ? ']' : ''}`).join('\n') || ' pass'}`).join('\n\n');
|
|
140
|
-
const overloads = model.components.map((component) => ` @overload\n def component(self, component: Literal[${JSON.stringify(component.name)}], *, key: str, **props: Unpack[${component.name}Props]) -> None: ...`).join('\n');
|
|
141
|
-
return `${header(model, '#')}from typing import Any, Literal, Required, TypedDict, Unpack, overload\nfrom omochi_ui import Catalog, UI\nfrom omochi_ui.catalog import DataBinding, DynamicBoolean, DynamicNumber, DynamicString, DynamicStringList, FunctionCall\n\n${body}\n\nclass ${model.name}UI(UI):\n${overloads}\n def component(self, component: str, *, key: str, **props: Any) -> None:\n super().component(component, key=key, **props)\n\n${model.name.toUpperCase()} = Catalog(id=${JSON.stringify(model.id)}, name=${JSON.stringify(model.name)}, ui_type=${model.name}UI)\n`;
|
|
142
|
-
}
|
|
143
|
-
function rust(model) {
|
|
144
|
-
const enumName = (component, property) => `${identifier(component.name)}${identifier(property.name[0].toUpperCase() + property.name.slice(1))}`;
|
|
145
|
-
const enums = model.components.flatMap((component) => component.properties.flatMap((property) => {
|
|
146
|
-
const values = JSON.parse(property.type).enum;
|
|
147
|
-
if (!Array.isArray(values))
|
|
148
|
-
return [];
|
|
149
|
-
const variants = values.map((value) => {
|
|
150
|
-
const raw = String(value);
|
|
151
|
-
const variant = identifier(raw[0].toUpperCase() + raw.slice(1)) || 'Value';
|
|
152
|
-
return ` #[serde(rename = ${JSON.stringify(raw)})]\n ${/^\d/.test(variant) ? `Value${variant}` : variant},`;
|
|
153
|
-
}).join('\n');
|
|
154
|
-
return [`#[derive(Clone, Debug, Serialize)]\npub enum ${enumName(component, property)} {\n${variants}\n}`];
|
|
155
|
-
})).join('\n\n');
|
|
156
|
-
const body = model.components.map((component) => `#[derive(Clone, Debug, Serialize)]\n#[serde(rename_all = "camelCase")]\npub struct ${component.name}Props {\n${component.properties.map((property) => {
|
|
157
|
-
const schema = JSON.parse(property.type);
|
|
158
|
-
const type = Array.isArray(schema.enum) ? enumName(component, property) : propertyType(property, 'rs');
|
|
159
|
-
return `${property.required ? '' : ' #[serde(skip_serializing_if = "Option::is_none")]\n'} pub ${snake(property.name)}: ${property.required ? type : `Option<${type}>`},`;
|
|
160
|
-
}).join('\n')}\n}\nimpl ComponentProps for ${component.name}Props {\n const NAME: &'static str = ${JSON.stringify(component.name)};\n fn into_props(self) -> JsonObject { serde_json::to_value(self).unwrap().as_object().unwrap().clone() }\n}`).join('\n\n');
|
|
161
|
-
return `${header(model, '//')}use omochi_ui::{ComponentProps, JsonObject};\nuse serde::Serialize;\nuse serde_json::Value;\n\npub const CATALOG_ID: &str = ${JSON.stringify(model.id)};\n\n${enums}${enums ? '\n\n' : ''}${body}\n`;
|
|
162
|
-
}
|
|
163
|
-
async function output(path, content, check) {
|
|
164
|
-
const current = await readFile(path, 'utf8').catch(() => null);
|
|
165
|
-
if (check) {
|
|
166
|
-
if (current !== content)
|
|
167
|
-
throw new Error(`generated catalog is stale: ${path}`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (current !== content)
|
|
171
|
-
await writeFile(path, content);
|
|
172
|
-
}
|
|
173
|
-
export async function generateCatalog(options) {
|
|
174
|
-
if (!options.typescript && !options.python && !options.rust)
|
|
175
|
-
throw new Error('at least one output flag is required');
|
|
176
|
-
const raw = await readFile(options.catalog, 'utf8');
|
|
177
|
-
const parsed = JSON.parse(raw);
|
|
178
|
-
const resolved = await resolveLocalRefs(parsed, parsed, dirname(resolve(options.catalog)));
|
|
179
|
-
const model = normalize(JSON.stringify(resolved), options.name, createHash('sha256').update(raw).digest('hex'));
|
|
180
|
-
await Promise.all([
|
|
181
|
-
options.typescript ? output(options.typescript, typescript(model), Boolean(options.check)) : undefined,
|
|
182
|
-
options.python ? output(options.python, python(model), Boolean(options.check)) : undefined,
|
|
183
|
-
options.rust ? output(options.rust, rust(model), Boolean(options.check)) : undefined,
|
|
184
|
-
]);
|
|
185
|
-
}
|
package/dist/cli.d.ts
DELETED