@oimdb/cli 0.0.0-stage → 1.1.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,3 +1,142 @@
1
- # Temporary Holding Version
1
+ # @oimdb/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The OIMDB command line. It reads the state of a live page, of a model module or of Chrome over the
4
+ DevTools Protocol, and runs the bridge a page connects to with the DevTools UI. An AI assistant that can run a
5
+ shell command needs nothing else; one that cannot uses `@oimdb/mcp`, which answers the same four questions as
6
+ MCP tools.
7
+
8
+ 📖 **[Full documentation](https://oimdb.org/)**
9
+
10
+ ## Commands
11
+
12
+ ```bash
13
+ npx @oimdb/cli inspect # every registered collection and computed, as JSON
14
+ npx @oimdb/cli dump # the same as readable text
15
+ npx @oimdb/cli collection <name> # one registered collection
16
+ npx @oimdb/cli computed <name> # one registered computed
17
+ npx @oimdb/cli serve [--no-open] # the bridge, the DevTools UI, /api/inspect and /api/dump
18
+ npx @oimdb/cli mcp # the MCP server (needs @oimdb/mcp installed)
19
+ ```
20
+
21
+ `inspect`, `dump`, `collection` and `computed` print to stdout and exit with `1` and a message on stderr when the
22
+ state cannot be read. Their output is exactly what the `@oimdb/mcp` tools `oimdb_inspect`, `oimdb_dump`,
23
+ `oimdb_collection` and `oimdb_computed` return.
24
+
25
+ ## Where the state comes from
26
+
27
+ | Flag | Source |
28
+ |---|---|
29
+ | *(none)* | a running `oimdb serve`, at `http://localhost:7433` (or `OIMDB_UI_PORT`) |
30
+ | `--url <url>` | a running `oimdb serve` elsewhere |
31
+ | `--model <module>` | a module that builds the model in this process (or `OIMDB_MODEL_MODULE`) |
32
+ | `--cdp [url]` | Chrome launched with `--remote-debugging-port=9222` (or `OIMDB_CDP_URL`); no page code needed beyond `window.__OIMDB_DEV__ = oimDevRegistry` |
33
+
34
+ ### A live page: `serve`, then read
35
+
36
+ ```bash
37
+ npx @oimdb/cli serve # opens the UI at http://localhost:7433
38
+ npx @oimdb/cli inspect # in another terminal
39
+ ```
40
+
41
+ The page connects to the bridge from its debug entry point:
42
+
43
+ ```ts
44
+ // src/debug.ts
45
+ import './store/users.debug';
46
+ import './store/tasks.debug';
47
+ import { oimDevRegistry } from '@oimdb/devtools';
48
+
49
+ if (typeof window !== 'undefined') {
50
+ (window as unknown as Record<string, unknown>).__OIMDB_DEV__ = oimDevRegistry;
51
+ oimDevRegistry.connect(); // connects to ws://localhost:7432
52
+ }
53
+ ```
54
+
55
+ ### A model module, no browser
56
+
57
+ The module builds the model in Node and exports an `oimDevRegistry` (or a default export, or a zero-arg factory
58
+ that returns one). Its services are built with `defaults: oimDevDefaults`:
59
+
60
+ ```ts
61
+ // oimdb-model.js (built JS, or run the server under a TS loader)
62
+ import { OIMDB, type TOIMEntitySlot, createOIMSlotContentEntityService, createOIMSlotCollectionService, OIMSlotContentPositionalArrayBucketService, OIMBucketCommands } from '@oimdb/core';
63
+ import { OIMDevRegistry, oimDevDefaults } from '@oimdb/devtools';
64
+
65
+ type TUser = { id: string; name: string; role: string };
66
+
67
+ const db = new OIMDB();
68
+ const usersService = createOIMSlotCollectionService({ defaults: oimDevDefaults, slotContentService: createOIMSlotContentEntityService<TUser>() });
69
+ const users = usersService.createStoreSlot(db);
70
+ // A bucket's slots are the caller's — a write to a user moves the value and
71
+ // files nothing, so whoever writes the user also calls
72
+ // `usersByRoleService.slotContentService.writeSlotContent(bucketSlot, entitySlots)`.
73
+ const usersByRoleService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<TUser>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
74
+ const usersByRole = usersByRoleService.createStoreSlot(db);
75
+
76
+ export const oimDevRegistry = new OIMDevRegistry();
77
+ // The bucket collection and its store slot — you constructed them, so you hand them over.
78
+ oimDevRegistry.collection('users', usersService, users, {
79
+ indexes: { byRole: { bucketCollectionService: usersByRoleService, bucketStoreSlot: usersByRole } },
80
+ });
81
+ ```
82
+
83
+ ```bash
84
+ npx @oimdb/cli inspect --model ./oimdb-model.js
85
+ ```
86
+
87
+ A registry is asked for only `inspect()` and `dumpString()` (`IOIMDevRegistry`), so this package depends on no
88
+ other `@oimdb` package. Entity **field names** appear only when the model holds at least one sample entity.
89
+
90
+ ## Ports, host and origins
91
+
92
+ | Variable | Default | What it sets |
93
+ |---|---|---|
94
+ | `OIMDB_WS_PORT` | `7432` | The bridge WebSocket port |
95
+ | `OIMDB_UI_PORT` | `7433` | The `serve` UI HTTP port, and where the read commands look by default |
96
+ | `OIMDB_HOST` | `127.0.0.1` | The interface `serve` binds to |
97
+ | `OIMDB_ALLOWED_ORIGINS` | *(empty)* | Extra page origins allowed onto the bridge, comma-separated |
98
+ | `OIMDB_MODEL_MODULE` | — | The model module, when `--model` is not given |
99
+ | `OIMDB_CDP_URL` | `http://localhost:9222` | The CDP endpoint, when `--cdp` names none |
100
+
101
+ **It binds to loopback by default, on purpose.** This port hands out the
102
+ application's entire data model, and a `WebSocketServer` built with no host
103
+ binds every interface, which would put the model on the café Wi-Fi.
104
+ Set `OIMDB_HOST=0.0.0.0` when the browser really runs
105
+ somewhere else (a container, a VM, a phone), and accept that everyone who can
106
+ route there can talk to it. Both servers print the bound host at startup.
107
+
108
+ **Origins are checked, and `'*'` is never allowed.** A request with no `Origin` header is
109
+ allowed (that is a non-browser client). A loopback origin is allowed
110
+ (`localhost`, `127.0.0.1`, `::1`, `[::1]`, `*.localhost`). Anything else is
111
+ allowed only if you listed it exactly in `OIMDB_ALLOWED_ORIGINS`. The literal
112
+ string `null` is always rejected, because a sandboxed iframe on any site in the world
113
+ sends exactly that. Only an allow-listed origin gets
114
+ `Access-Control-Allow-Origin` echoed back.
115
+
116
+ While bound to a loopback host, the UI server also rejects a request whose
117
+ `Host` header is not a loopback authority, with **421 Misdirected request**.
118
+ This is the DNS-rebinding guard, and it is switched off when you bind elsewhere.
119
+
120
+ `serve` answers `/`, `/index.html`, `/app.js`, `/app.css`, `/api/inspect` (`{ connected, tab, data }`) and
121
+ `/api/dump` (`{ connected, data }`).
122
+
123
+ - **The newest browser tab takes the bridge**, and the previous socket is closed
124
+ with "Replaced by a newer OIMDB DevTools tab". Only the socket that currently
125
+ holds the bridge may answer a request; anything else is ignored.
126
+ - **A query times out after 5 seconds**, reported as "Browser response timeout
127
+ (5s). Is the page active?".
128
+ - **A malformed frame drops that client** with close code 1007, and `EADDRINUSE`
129
+ exits the process with a message naming the env var to change.
130
+
131
+ ## From your own process
132
+
133
+ ```ts
134
+ import { OIMDevStateReader, OIMDevModuleStateSource, OIMDevUIServer } from '@oimdb/cli';
135
+
136
+ const devStateReader = new OIMDevStateReader(new OIMDevModuleStateSource('./oimdb-model.js'));
137
+ console.log(await devStateReader.readInspectText());
138
+ ```
139
+
140
+ The sources are `OIMDevRegistryStateSource`, `OIMDevModuleStateSource`, `OIMDevBridgeStateSource`,
141
+ `OIMDevHttpStateSource` and `OIMDevCdpStateSource`, all `IOIMDevStateSource`. `OIMDevUIServer` and
142
+ `OIMDevBridge` are the servers `serve` runs.
package/bin/oimdb.cjs ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const {
4
+ OIMDevUIServer, OIMDevStateReader, OIMDevModuleStateSource, OIMDevHttpStateSource, OIMDevCdpStateSource,
5
+ } = require('../dist/index.cjs');
6
+
7
+ const USAGE = [
8
+ 'Usage:',
9
+ ' oimdb inspect every registered collection and computed, as JSON',
10
+ ' oimdb dump the same as readable text',
11
+ ' oimdb collection <name> one registered collection',
12
+ ' oimdb computed <name> one registered computed',
13
+ ' oimdb serve [--no-open] the bridge a page connects to, the DevTools UI and /api/inspect, /api/dump',
14
+ ' oimdb mcp the MCP server (needs @oimdb/mcp installed)',
15
+ '',
16
+ 'Where the read commands get the state (one of):',
17
+ ' --model <module> a module that builds the model in this process',
18
+ ' --cdp [url] Chrome over the DevTools Protocol (default http://localhost:9222)',
19
+ ' --url <url> a running "oimdb serve" (default http://localhost:7433)',
20
+ ].join('\n');
21
+
22
+ const args = process.argv.slice(2);
23
+ const command = args[0];
24
+
25
+ const flagValue = (name) => {
26
+ const at = args.indexOf(name);
27
+ if (at < 0) return undefined;
28
+ const next = args[at + 1];
29
+ return next === undefined || next.startsWith('--') ? '' : next;
30
+ };
31
+
32
+ const envList = (name) => (process.env[name] || '').split(',').map(entry => entry.trim()).filter(Boolean);
33
+
34
+ const devStateSourceOf = () => {
35
+ const modelModulePath = flagValue('--model') ?? process.env.OIMDB_MODEL_MODULE;
36
+ if (modelModulePath) return new OIMDevModuleStateSource(modelModulePath);
37
+ const cdpUrl = flagValue('--cdp');
38
+ if (cdpUrl !== undefined) return new OIMDevCdpStateSource(cdpUrl || process.env.OIMDB_CDP_URL || 'http://localhost:9222');
39
+ const uiPort = process.env.OIMDB_UI_PORT || '7433';
40
+ return new OIMDevHttpStateSource(flagValue('--url') || `http://localhost:${uiPort}`);
41
+ };
42
+
43
+ const read = async (readText) => {
44
+ try {
45
+ process.stdout.write(await readText(new OIMDevStateReader(devStateSourceOf())) + '\n');
46
+ } catch (error) {
47
+ process.stderr.write(`${error && error.message ? error.message : String(error)}\n`);
48
+ process.exit(1);
49
+ }
50
+ };
51
+
52
+ const requireName = () => {
53
+ const name = args[1];
54
+ if (name === undefined || name.startsWith('--')) {
55
+ process.stderr.write(`oimdb ${command}: name the ${command}.\n\n${USAGE}\n`);
56
+ process.exit(1);
57
+ }
58
+ return name;
59
+ };
60
+
61
+ if (command === 'inspect') {
62
+ read(reader => reader.readInspectText());
63
+ } else if (command === 'dump') {
64
+ read(reader => reader.readDumpText());
65
+ } else if (command === 'collection') {
66
+ const name = requireName();
67
+ read(reader => reader.readCollectionText(name));
68
+ } else if (command === 'computed') {
69
+ const name = requireName();
70
+ read(reader => reader.readComputedText(name));
71
+ } else if (command === 'serve') {
72
+ new OIMDevUIServer({
73
+ wsPort: process.env.OIMDB_WS_PORT ? Number(process.env.OIMDB_WS_PORT) : 7432,
74
+ uiPort: process.env.OIMDB_UI_PORT ? Number(process.env.OIMDB_UI_PORT) : 7433,
75
+ uiDir: path.join(__dirname, '../ui'),
76
+ open: !args.includes('--no-open'),
77
+ host: process.env.OIMDB_HOST || '127.0.0.1',
78
+ allowedOrigins: envList('OIMDB_ALLOWED_ORIGINS'),
79
+ }).start();
80
+ } else if (command === 'mcp') {
81
+ let mcp;
82
+ try {
83
+ mcp = require('@oimdb/mcp');
84
+ } catch {
85
+ process.stderr.write('oimdb mcp: install @oimdb/mcp next to @oimdb/cli.\n');
86
+ process.exit(1);
87
+ }
88
+ mcp.createOIMDevMcpServer({
89
+ wsPort: process.env.OIMDB_WS_PORT ? Number(process.env.OIMDB_WS_PORT) : 7432,
90
+ modelModulePath: flagValue('--model') || process.env.OIMDB_MODEL_MODULE || undefined,
91
+ host: process.env.OIMDB_HOST || '127.0.0.1',
92
+ allowedOrigins: envList('OIMDB_ALLOWED_ORIGINS'),
93
+ }).start().catch(error => {
94
+ process.stderr.write(`[OIMDB MCP] Fatal: ${error.message}\n`);
95
+ process.exit(1);
96
+ });
97
+ } else {
98
+ process.stderr.write(`${USAGE}\n`);
99
+ process.exit(command === undefined || command === '--help' || command === 'help' ? 0 : 1);
100
+ }