@omochi-ui/cli 0.1.1 → 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 CHANGED
@@ -1,279 +1,65 @@
1
- # CLI reference
1
+ # Omochi native services
2
2
 
3
- [Overview](../../README.md) · [Architecture](../../docs/architecture.md) · [Discovery guide](../../docs/discovery.md)
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
- Build the workspace with `bun install` and `bun run build`. Run commands from its
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
- ```sh
10
- bun run omochi-ui --help
11
- bun run omochi-ui hub --help
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
- No arguments prints the overview. `-h`, `--help`, and `help <command>` print help
16
- to stdout and exit successfully without starting services or changing config.
17
- Invalid arguments print usage to stderr and exit with code 2.
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
- ## Command map
18
+ ## Pairing
20
19
 
21
- | Task | Command | Browser UI included? |
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
- ## up
22
+ omochi-ui gateway --remote --pair --name workstation
33
23
 
34
- Start local services, the hub, and a read-only dashboard preset from TOML.
24
+ Import its invitation on the hub PC:
35
25
 
36
- ```sh
37
- bun run omochi-ui up --open
38
- bun run omochi-ui up --port 5557 ./examples/quickstart/omochi.toml
39
- ```
26
+ omochi-ui pair --name workstation
27
+ # Paste the invitation on stdin, then Ctrl+D.
28
+ omochi-ui hub
40
29
 
41
- The file defaults to `./omochi.toml`. A service uses either a literal `command`
42
- or an `omochi-utils` shorthand. Commands do not use a shell; relative working
43
- directories are resolved from the composition file. Child services receive
44
- `OMOCHI_SERVICE` and remain independent: one exiting leaves its panel unavailable
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
- ```toml
50
- version = 1
51
- id = "local-development"
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
- [services.health]
54
- utility = ["http", "http://127.0.0.1:5173/"]
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
- [services.app]
57
- command = ["uv", "run", "app.py"]
58
- cwd = "./backend"
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
- [dashboard]
61
- name = "Local development"
47
+ ## Breaking upgrade
62
48
 
63
- [[dashboard.rows]]
64
- panels = ["health", "app"]
65
- height = "medium"
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
- [dashboard.panels.health]
68
- title = "Health"
69
- service = "health"
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
- [dashboard.panels.app]
72
- title = "Application"
73
- service = "app"
74
- ```
58
+ ## Registry distributions
75
59
 
76
- Rows divide the twelve-column dashboard evenly. Heights are `small`, `medium`,
77
- or `large`; a panel's surface defaults to `main`. Presets do not modify saved
78
- dashboard storage. Use **Save as dashboard** in the hub to make an editable copy.
79
- Ctrl+C stops only the services and gateway started by this command.
80
-
81
- | Option | Meaning | Default |
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
- "publishConfig": {
4
- "access": "public",
5
- "registry": "https://registry.npmjs.org/"
6
- },
3
+ "version": "0.2.0",
4
+ "description": "Native Omochi cli",
5
+ "files": [
6
+ "bin"
7
+ ],
7
8
  "engines": {
8
- "bun": ">=1.4.3"
9
+ "node": ">=22"
9
10
  },
10
- "version": "0.1.1",
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.js"
12
+ "omochi-ui": "./bin/omochi-ui.cjs"
16
13
  },
17
- "files": [
18
- "bin",
19
- "dist"
20
- ],
21
- "dependencies": {
22
- "@a2ui/web_core": "0.10.5",
23
- "@omochi-ui/core": "0.1.1",
24
- "@omochi-ui/discovery": "0.1.1",
25
- "@omochi-ui/hub-ui": "0.1.1",
26
- "@omochi-ui/utils": "0.1.1",
27
- "smol-toml": "^1.9.0",
28
- "ws": "^8.18.0"
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/k-kozika/omochi.git",
33
- "directory": "packages/cli"
30
+ "url": "https://github.com/omochi-ui/omochi.git"
34
31
  },
35
- "homepage": "https://github.com/k-kozika/omochi#readme",
36
- "bugs": {
37
- "url": "https://github.com/k-kozika/omochi/issues"
32
+ "publishConfig": {
33
+ "access": "public"
38
34
  }
39
35
  }
package/bin/omochi-ui.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- import '../dist/cli.js'
@@ -1,9 +0,0 @@
1
- export interface GenerateCatalogOptions {
2
- catalog: string;
3
- name?: string;
4
- typescript?: string;
5
- python?: string;
6
- rust?: string;
7
- check?: boolean;
8
- }
9
- export declare function generateCatalog(options: GenerateCatalogOptions): Promise<void>;
@@ -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
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};