@omochi-ui/utils 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 +65 -202
- package/bin/omochi-utils.cjs +34 -0
- package/package.json +23 -25
- package/bin/omochi-utils.js +0 -2
- package/dist/button.d.ts +0 -20
- package/dist/button.js +0 -51
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -239
- package/dist/command.d.ts +0 -28
- package/dist/command.js +0 -110
- package/dist/events.d.ts +0 -23
- package/dist/events.js +0 -54
- package/dist/http-body.d.ts +0 -16
- package/dist/http-body.js +0 -59
- package/dist/http.d.ts +0 -32
- package/dist/http.js +0 -156
- package/dist/mcp.d.ts +0 -3
- package/dist/mcp.js +0 -59
- package/dist/output.d.ts +0 -10
- package/dist/output.js +0 -35
- package/dist/process.d.ts +0 -32
- package/dist/process.js +0 -115
- package/dist/publisher.d.ts +0 -31
- package/dist/publisher.js +0 -101
- package/dist/stream.d.ts +0 -18
- package/dist/stream.js +0 -46
- package/dist/surface.d.ts +0 -18
- package/dist/surface.js +0 -51
- package/dist/tail.d.ts +0 -25
- package/dist/tail.js +0 -130
- package/dist/watch.d.ts +0 -37
- package/dist/watch.js +0 -130
package/dist/http.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { Action } from "@omochi-ui/sdk";
|
|
2
|
-
import { OutputBuffer, fenced, formatDuration } from "./output.js";
|
|
3
|
-
import { readText } from "./http-body.js";
|
|
4
|
-
import { UtilitySurface } from "./surface.js";
|
|
5
|
-
const BODY_LIMIT = 64 * 1024;
|
|
6
|
-
export class HttpWatch extends UtilitySurface {
|
|
7
|
-
options;
|
|
8
|
-
fetcher;
|
|
9
|
-
body = new OutputBuffer();
|
|
10
|
-
interval;
|
|
11
|
-
controller;
|
|
12
|
-
running = false;
|
|
13
|
-
paused = false;
|
|
14
|
-
status = "Starting…";
|
|
15
|
-
metadata = "";
|
|
16
|
-
consecutiveFailures = 0;
|
|
17
|
-
lastHealthy;
|
|
18
|
-
transition = "No previous result";
|
|
19
|
-
constructor(publisher, options) {
|
|
20
|
-
super(publisher, options.name);
|
|
21
|
-
this.options = options;
|
|
22
|
-
this.fetcher = options.fetch ?? fetch;
|
|
23
|
-
}
|
|
24
|
-
async start() {
|
|
25
|
-
await super.start();
|
|
26
|
-
void this.run();
|
|
27
|
-
}
|
|
28
|
-
build(ui) {
|
|
29
|
-
ui.text(this.name, { key: "title", variant: "h3" });
|
|
30
|
-
ui.row({ key: "controls" }, () => {
|
|
31
|
-
if (this.paused)
|
|
32
|
-
ui.button("Resume", new Action("resume").call(null), { key: "resume", variant: "primary" });
|
|
33
|
-
else
|
|
34
|
-
ui.button("Pause", new Action("pause").call(null), { key: "pause" });
|
|
35
|
-
if (!this.running)
|
|
36
|
-
ui.button("Run now", new Action("run").call(null), { key: "run" });
|
|
37
|
-
});
|
|
38
|
-
ui.text(`${this.status}${this.paused ? " · Paused" : ""}`, { key: "status" });
|
|
39
|
-
if (this.metadata)
|
|
40
|
-
ui.text(this.metadata, { key: "metadata" });
|
|
41
|
-
if (this.body.text)
|
|
42
|
-
ui.text(fenced(this.body.text, this.body.truncated), { key: "body" });
|
|
43
|
-
}
|
|
44
|
-
onAction(name) {
|
|
45
|
-
if (name === "pause" && !this.paused) {
|
|
46
|
-
this.paused = true;
|
|
47
|
-
clearTimeout(this.interval);
|
|
48
|
-
this.scheduleRender();
|
|
49
|
-
}
|
|
50
|
-
else if (name === "resume" && this.paused) {
|
|
51
|
-
this.paused = false;
|
|
52
|
-
this.scheduleRender();
|
|
53
|
-
if (!this.running)
|
|
54
|
-
void this.run();
|
|
55
|
-
}
|
|
56
|
-
else if (name === "run" && !this.running) {
|
|
57
|
-
clearTimeout(this.interval);
|
|
58
|
-
void this.run();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
close() {
|
|
62
|
-
this.closeSurface();
|
|
63
|
-
clearTimeout(this.interval);
|
|
64
|
-
this.controller?.abort();
|
|
65
|
-
return Promise.resolve();
|
|
66
|
-
}
|
|
67
|
-
async run() {
|
|
68
|
-
if (this.running || this.closing)
|
|
69
|
-
return;
|
|
70
|
-
this.running = true;
|
|
71
|
-
this.status = "Requesting…";
|
|
72
|
-
this.scheduleRender();
|
|
73
|
-
const startedAt = performance.now();
|
|
74
|
-
const controller = new AbortController();
|
|
75
|
-
this.controller = controller;
|
|
76
|
-
const timeout = setTimeout(() => controller.abort(new Error("request timed out")), this.options.timeoutMs);
|
|
77
|
-
let healthy = false;
|
|
78
|
-
try {
|
|
79
|
-
const response = await this.fetcher(this.options.url, { signal: controller.signal, redirect: "follow" });
|
|
80
|
-
const durationMs = performance.now() - startedAt;
|
|
81
|
-
const contentType = response.headers.get("content-type") ?? "unknown";
|
|
82
|
-
const contentLength = response.headers.get("content-length");
|
|
83
|
-
const textual = isText(contentType);
|
|
84
|
-
let rawBody = "";
|
|
85
|
-
let bodyTruncated = false;
|
|
86
|
-
let expectedBodyMatched = this.options.expectBody === undefined;
|
|
87
|
-
if (textual) {
|
|
88
|
-
const read = await readText(response, BODY_LIMIT, this.options.expectBody);
|
|
89
|
-
rawBody = read.text;
|
|
90
|
-
bodyTruncated = read.truncated;
|
|
91
|
-
expectedBodyMatched = read.expectedMatched;
|
|
92
|
-
let display = rawBody;
|
|
93
|
-
if (isJson(contentType) && !bodyTruncated) {
|
|
94
|
-
try {
|
|
95
|
-
display = JSON.stringify(JSON.parse(rawBody), null, 2);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
display = `Invalid JSON response\n\n${rawBody}`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
this.body.reset(display);
|
|
102
|
-
if (bodyTruncated)
|
|
103
|
-
this.body.append("\n\n[Response body truncated]");
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
await response.body?.cancel();
|
|
107
|
-
this.body.reset();
|
|
108
|
-
}
|
|
109
|
-
const statusMatches = response.status >= this.options.expectStatus[0] && response.status <= this.options.expectStatus[1];
|
|
110
|
-
const bodyMatches = textual && expectedBodyMatched || this.options.expectBody === undefined;
|
|
111
|
-
healthy = statusMatches && bodyMatches;
|
|
112
|
-
this.status = `${healthy ? "Healthy" : "Unhealthy"} · HTTP ${response.status} · ${formatDuration(durationMs)} · ${new Date().toISOString()}`;
|
|
113
|
-
this.metadata = [
|
|
114
|
-
`URL: ${response.url}`,
|
|
115
|
-
`Content-Type: ${contentType}`,
|
|
116
|
-
`Size: ${contentLength ? `${contentLength} bytes` : textual && !bodyTruncated ? `${Buffer.byteLength(rawBody)} bytes` : "unknown"}`,
|
|
117
|
-
`Expectation: status ${this.options.expectStatus[0]}${this.options.expectStatus[0] === this.options.expectStatus[1] ? "" : `–${this.options.expectStatus[1]}`}${this.options.expectBody === undefined ? "" : ` and body contains ${JSON.stringify(this.options.expectBody)}`}`,
|
|
118
|
-
].join(" \n");
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
const durationMs = performance.now() - startedAt;
|
|
122
|
-
const message = controller.signal.aborted && controller.signal.reason instanceof Error
|
|
123
|
-
? controller.signal.reason.message
|
|
124
|
-
: error instanceof Error ? error.message : String(error);
|
|
125
|
-
this.status = `Unhealthy · ${message} · ${formatDuration(durationMs)} · ${new Date().toISOString()}`;
|
|
126
|
-
this.metadata = `URL: ${this.options.url}`;
|
|
127
|
-
this.body.reset();
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
clearTimeout(timeout);
|
|
131
|
-
this.controller = undefined;
|
|
132
|
-
this.running = false;
|
|
133
|
-
}
|
|
134
|
-
if (healthy)
|
|
135
|
-
this.consecutiveFailures = 0;
|
|
136
|
-
else
|
|
137
|
-
this.consecutiveFailures++;
|
|
138
|
-
if (this.lastHealthy === undefined)
|
|
139
|
-
this.transition = healthy ? "Initial result: healthy" : "Initial result: unhealthy";
|
|
140
|
-
else if (this.lastHealthy !== healthy)
|
|
141
|
-
this.transition = healthy ? "Recovered" : "Became unhealthy";
|
|
142
|
-
this.lastHealthy = healthy;
|
|
143
|
-
this.metadata += ` \nConsecutive failures: ${this.consecutiveFailures} \nState: ${this.transition}`;
|
|
144
|
-
this.scheduleRender();
|
|
145
|
-
if (!this.paused && !this.closing)
|
|
146
|
-
this.interval = setTimeout(() => void this.run(), this.options.intervalMs);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function isJson(contentType) {
|
|
150
|
-
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
151
|
-
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
152
|
-
}
|
|
153
|
-
function isText(contentType) {
|
|
154
|
-
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
155
|
-
return mediaType.startsWith("text/") || isJson(contentType) || ["application/javascript", "application/xml", "application/x-www-form-urlencoded"].includes(mediaType) || mediaType.endsWith("+xml");
|
|
156
|
-
}
|
package/dist/mcp.d.ts
DELETED
package/dist/mcp.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { BASIC_CATALOG } from "@omochi-ui/sdk";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
const examples = {
|
|
6
|
-
publish: [
|
|
7
|
-
{ version: "v0.9", createSurface: { surfaceId: "main", catalogId: BASIC_CATALOG.id, sendDataModel: true } },
|
|
8
|
-
{ version: "v0.9", updateComponents: { surfaceId: "main", components: [
|
|
9
|
-
{ id: "root", component: "Column", children: ["title", "input", "submit"] },
|
|
10
|
-
{ id: "title", component: "Text", text: "What should we build?" },
|
|
11
|
-
{ id: "input", component: "TextField", label: "Request", value: { path: "/request" } },
|
|
12
|
-
{ id: "submit", component: "Button", child: "submit-label", action: { event: { name: "submit", context: { request: { path: "/request" } } } } },
|
|
13
|
-
{ id: "submit-label", component: "Text", text: "Send" },
|
|
14
|
-
] } },
|
|
15
|
-
{ version: "v0.9", updateDataModel: { surfaceId: "main", path: "/", value: { request: "" } } },
|
|
16
|
-
],
|
|
17
|
-
update: [{ version: "v0.9", updateComponents: { surfaceId: "main", components: [{ id: "title", component: "Text", text: "Working…" }] } }],
|
|
18
|
-
delete: [{ version: "v0.9", deleteSurface: { surfaceId: "main" } }],
|
|
19
|
-
};
|
|
20
|
-
function result(value) {
|
|
21
|
-
return { content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: { ...value } };
|
|
22
|
-
}
|
|
23
|
-
async function tool(work) {
|
|
24
|
-
try {
|
|
25
|
-
return result(await work());
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
return { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export async function startMcp(publisher, onClose) {
|
|
32
|
-
const server = new McpServer({ name: "omochi-utils", version: "0.1.0" }, {
|
|
33
|
-
instructions: `Publish A2UI v0.9 surfaces to the local Omochi gateway. Basic Catalog: ${BASIC_CATALOG.id}. ` +
|
|
34
|
-
"Use surface-local IDs (no gateway prefixes). Surfaces live only while this process runs. " +
|
|
35
|
-
"Call read_events to receive button/form actions, then publish_messages to respond; events do not execute code. " +
|
|
36
|
-
`Examples for publish_messages({messages: ...}): ${JSON.stringify(examples)}`,
|
|
37
|
-
});
|
|
38
|
-
server.registerTool("publish_messages", {
|
|
39
|
-
description: `Publish an ordered, prevalidated batch of A2UI v0.9 createSurface/updateComponents/updateDataModel/deleteSurface messages. Basic Catalog: ${BASIC_CATALOG.id}. Example: ${JSON.stringify(examples.publish)}`,
|
|
40
|
-
inputSchema: { messages: z.array(z.record(z.string(), z.unknown())).min(1) },
|
|
41
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
42
|
-
}, ({ messages }) => tool(() => publisher.publish(messages)));
|
|
43
|
-
server.registerTool("list_surfaces", {
|
|
44
|
-
description: "List this process's service ID and currently published surface IDs.",
|
|
45
|
-
inputSchema: {},
|
|
46
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
47
|
-
}, () => tool(() => publisher.list()));
|
|
48
|
-
server.registerTool("read_events", {
|
|
49
|
-
description: "Read button/form actions without consuming them. Start after=0, then use nextCursor. timeoutMs waits up to 30000ms. Latest 1000 events retained; dropped reports missing events. The original A2UI message includes action.context and optional client data model metadata.",
|
|
50
|
-
inputSchema: {
|
|
51
|
-
after: z.number().int().nonnegative().optional(),
|
|
52
|
-
timeoutMs: z.number().int().min(0).max(30_000).optional(),
|
|
53
|
-
},
|
|
54
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
55
|
-
}, ({ after, timeoutMs }, extra) => tool(() => publisher.events.read(after, timeoutMs, extra.signal)));
|
|
56
|
-
server.server.onclose = onClose;
|
|
57
|
-
await server.connect(new StdioServerTransport());
|
|
58
|
-
return server;
|
|
59
|
-
}
|
package/dist/output.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export declare class OutputBuffer {
|
|
2
|
-
private value;
|
|
3
|
-
private wasTruncated;
|
|
4
|
-
get text(): string;
|
|
5
|
-
get truncated(): boolean;
|
|
6
|
-
reset(value?: string): void;
|
|
7
|
-
append(chunk: string): void;
|
|
8
|
-
}
|
|
9
|
-
export declare function fenced(text: string, truncated?: boolean): string;
|
|
10
|
-
export declare function formatDuration(milliseconds: number): string;
|
package/dist/output.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const OUTPUT_LIMIT = 64 * 1024;
|
|
2
|
-
export class OutputBuffer {
|
|
3
|
-
value = "";
|
|
4
|
-
wasTruncated = false;
|
|
5
|
-
get text() { return this.value; }
|
|
6
|
-
get truncated() { return this.wasTruncated; }
|
|
7
|
-
reset(value = "") {
|
|
8
|
-
this.value = "";
|
|
9
|
-
this.wasTruncated = false;
|
|
10
|
-
this.append(value);
|
|
11
|
-
}
|
|
12
|
-
append(chunk) {
|
|
13
|
-
const bytes = Buffer.from(this.value + chunk);
|
|
14
|
-
if (bytes.length <= OUTPUT_LIMIT) {
|
|
15
|
-
this.value += chunk;
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
let start = bytes.length - OUTPUT_LIMIT;
|
|
19
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80)
|
|
20
|
-
start++;
|
|
21
|
-
this.value = bytes.subarray(start).toString("utf8");
|
|
22
|
-
this.wasTruncated = true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function fenced(text, truncated = false) {
|
|
26
|
-
const fence = "`".repeat(Math.max(3, ...[...text.matchAll(/`+/g)].map((match) => match[0].length + 1)));
|
|
27
|
-
return `${truncated ? "Earlier output truncated.\n\n" : ""}${fence}\n${text}\n${fence}`;
|
|
28
|
-
}
|
|
29
|
-
export function formatDuration(milliseconds) {
|
|
30
|
-
if (milliseconds < 1000)
|
|
31
|
-
return `${Math.round(milliseconds)} ms`;
|
|
32
|
-
if (milliseconds < 60_000)
|
|
33
|
-
return `${(milliseconds / 1000).toFixed(milliseconds < 10_000 ? 2 : 1)} s`;
|
|
34
|
-
return `${(milliseconds / 60_000).toFixed(1)} min`;
|
|
35
|
-
}
|
package/dist/process.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type UI } from "@omochi-ui/sdk";
|
|
2
|
-
import { Publisher } from "./publisher.js";
|
|
3
|
-
import { UtilitySurface } from "./surface.js";
|
|
4
|
-
export type RestartPolicy = "never" | "on-failure" | "always";
|
|
5
|
-
export interface ProcessOptions {
|
|
6
|
-
command: string[];
|
|
7
|
-
cwd?: string;
|
|
8
|
-
name: string;
|
|
9
|
-
restart: RestartPolicy;
|
|
10
|
-
restartDelayMs: number;
|
|
11
|
-
}
|
|
12
|
-
export declare class CommandProcess extends UtilitySurface {
|
|
13
|
-
private readonly options;
|
|
14
|
-
private readonly output;
|
|
15
|
-
private readonly runner;
|
|
16
|
-
private running;
|
|
17
|
-
private status;
|
|
18
|
-
private restartCount;
|
|
19
|
-
private suppressRestart;
|
|
20
|
-
private restartTimer?;
|
|
21
|
-
private control;
|
|
22
|
-
private closePromise?;
|
|
23
|
-
constructor(publisher: Publisher, options: ProcessOptions);
|
|
24
|
-
protected build(ui: UI): void;
|
|
25
|
-
protected onAction(name: string): void;
|
|
26
|
-
close(): Promise<void>;
|
|
27
|
-
private enqueue;
|
|
28
|
-
private launch;
|
|
29
|
-
private stop;
|
|
30
|
-
private restart;
|
|
31
|
-
private exited;
|
|
32
|
-
}
|
package/dist/process.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { Action } from "@omochi-ui/sdk";
|
|
2
|
-
import { ManagedCommand, describeResult } from "./command.js";
|
|
3
|
-
import { OutputBuffer, fenced } from "./output.js";
|
|
4
|
-
import { UtilitySurface } from "./surface.js";
|
|
5
|
-
export class CommandProcess extends UtilitySurface {
|
|
6
|
-
options;
|
|
7
|
-
output = new OutputBuffer();
|
|
8
|
-
runner;
|
|
9
|
-
running = false;
|
|
10
|
-
status = "Stopped";
|
|
11
|
-
restartCount = 0;
|
|
12
|
-
suppressRestart = false;
|
|
13
|
-
restartTimer;
|
|
14
|
-
control = Promise.resolve();
|
|
15
|
-
closePromise;
|
|
16
|
-
constructor(publisher, options) {
|
|
17
|
-
super(publisher, options.name);
|
|
18
|
-
this.options = options;
|
|
19
|
-
this.runner = new ManagedCommand({
|
|
20
|
-
command: options.command,
|
|
21
|
-
cwd: options.cwd,
|
|
22
|
-
onData: (chunk) => {
|
|
23
|
-
this.output.append(chunk);
|
|
24
|
-
this.scheduleRender();
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
build(ui) {
|
|
29
|
-
ui.text(this.name, { key: "title", variant: "h3" });
|
|
30
|
-
ui.row({ key: "controls" }, () => {
|
|
31
|
-
if (this.running) {
|
|
32
|
-
ui.button("Stop", new Action("stop").call(null), { key: "stop" });
|
|
33
|
-
ui.button("Restart", new Action("restart").call(null), { key: "restart", variant: "primary" });
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
ui.button("Start", new Action("start").call(null), { key: "start", variant: "primary" });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
ui.text(`${this.status} · automatic restarts: ${this.restartCount}`, { key: "status" });
|
|
40
|
-
ui.text(fenced(this.output.text, this.output.truncated), { key: "output" });
|
|
41
|
-
}
|
|
42
|
-
onAction(name) {
|
|
43
|
-
if (name === "start")
|
|
44
|
-
this.enqueue(async () => this.launch(false));
|
|
45
|
-
if (name === "stop")
|
|
46
|
-
this.enqueue(() => this.stop());
|
|
47
|
-
if (name === "restart")
|
|
48
|
-
this.enqueue(() => this.restart());
|
|
49
|
-
}
|
|
50
|
-
close() {
|
|
51
|
-
if (this.closePromise)
|
|
52
|
-
return this.closePromise;
|
|
53
|
-
this.closeSurface();
|
|
54
|
-
this.suppressRestart = true;
|
|
55
|
-
clearTimeout(this.restartTimer);
|
|
56
|
-
this.closePromise = (async () => {
|
|
57
|
-
await this.control;
|
|
58
|
-
await this.runner.close();
|
|
59
|
-
})();
|
|
60
|
-
return this.closePromise;
|
|
61
|
-
}
|
|
62
|
-
enqueue(operation) {
|
|
63
|
-
this.control = this.control.then(operation).catch((error) => {
|
|
64
|
-
this.status = `Control failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
65
|
-
this.scheduleRender();
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
launch(automatic) {
|
|
69
|
-
if (this.running || this.closing)
|
|
70
|
-
return;
|
|
71
|
-
clearTimeout(this.restartTimer);
|
|
72
|
-
this.suppressRestart = false;
|
|
73
|
-
this.running = true;
|
|
74
|
-
if (automatic)
|
|
75
|
-
this.restartCount++;
|
|
76
|
-
this.output.append(`\n--- ${automatic ? "Automatically restarted" : "Started"} at ${new Date().toISOString()} ---\n`);
|
|
77
|
-
this.status = "Running…";
|
|
78
|
-
this.scheduleRender();
|
|
79
|
-
void this.runner.start().then((result) => this.exited(result));
|
|
80
|
-
}
|
|
81
|
-
async stop() {
|
|
82
|
-
this.suppressRestart = true;
|
|
83
|
-
clearTimeout(this.restartTimer);
|
|
84
|
-
if (this.running)
|
|
85
|
-
await this.runner.stop();
|
|
86
|
-
this.status = "Stopped";
|
|
87
|
-
this.scheduleRender();
|
|
88
|
-
}
|
|
89
|
-
async restart() {
|
|
90
|
-
this.suppressRestart = true;
|
|
91
|
-
clearTimeout(this.restartTimer);
|
|
92
|
-
if (this.running)
|
|
93
|
-
await this.runner.stop();
|
|
94
|
-
this.suppressRestart = false;
|
|
95
|
-
this.launch(false);
|
|
96
|
-
}
|
|
97
|
-
exited(result) {
|
|
98
|
-
this.running = false;
|
|
99
|
-
const description = describeResult(result);
|
|
100
|
-
this.output.append(`\n--- ${description} at ${new Date().toISOString()} ---\n`);
|
|
101
|
-
this.status = description;
|
|
102
|
-
this.scheduleRender();
|
|
103
|
-
if (this.closing || this.suppressRestart || !shouldRestart(this.options.restart, result))
|
|
104
|
-
return;
|
|
105
|
-
this.status = `${description} · restarting soon…`;
|
|
106
|
-
this.restartTimer = setTimeout(() => this.launch(true), this.options.restartDelayMs);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function shouldRestart(policy, result) {
|
|
110
|
-
if (policy === "never")
|
|
111
|
-
return false;
|
|
112
|
-
if (policy === "always")
|
|
113
|
-
return true;
|
|
114
|
-
return Boolean(result.error || result.signal || result.code !== 0);
|
|
115
|
-
}
|
package/dist/publisher.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { JsonObject } from "@omochi-ui/sdk";
|
|
2
|
-
import { UnixSocketServerTransport, type UnixSocketServerTransportOptions } from "@omochi-ui/sdk/node";
|
|
3
|
-
import { EventBuffer } from "./events.js";
|
|
4
|
-
export type PublisherOptions = Omit<UnixSocketServerTransportOptions, "serviceId"> & {
|
|
5
|
-
serviceId?: string;
|
|
6
|
-
};
|
|
7
|
-
export declare class Publisher {
|
|
8
|
-
readonly serviceId: string;
|
|
9
|
-
readonly transport: UnixSocketServerTransport;
|
|
10
|
-
readonly events: EventBuffer;
|
|
11
|
-
private snapshot;
|
|
12
|
-
private queue;
|
|
13
|
-
private closing;
|
|
14
|
-
private closePromise?;
|
|
15
|
-
private readonly receiver;
|
|
16
|
-
private readonly listeners;
|
|
17
|
-
private constructor();
|
|
18
|
-
static open(options?: PublisherOptions): Promise<Publisher>;
|
|
19
|
-
onAction(listener: (message: JsonObject) => void): () => void;
|
|
20
|
-
list(): {
|
|
21
|
-
serviceId: string;
|
|
22
|
-
surfaceIds: string[];
|
|
23
|
-
};
|
|
24
|
-
publish(messages: unknown[]): Promise<{
|
|
25
|
-
accepted: number;
|
|
26
|
-
serviceId: string;
|
|
27
|
-
surfaceIds: string[];
|
|
28
|
-
}>;
|
|
29
|
-
close(): Promise<void>;
|
|
30
|
-
private receive;
|
|
31
|
-
}
|
package/dist/publisher.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { A2uiSnapshot } from "@omochi-ui/core";
|
|
3
|
-
import { UnixSocketServerTransport } from "@omochi-ui/sdk/node";
|
|
4
|
-
import { EventBuffer } from "./events.js";
|
|
5
|
-
export class Publisher {
|
|
6
|
-
serviceId;
|
|
7
|
-
transport;
|
|
8
|
-
events = new EventBuffer();
|
|
9
|
-
snapshot = new A2uiSnapshot();
|
|
10
|
-
queue = Promise.resolve();
|
|
11
|
-
closing = false;
|
|
12
|
-
closePromise;
|
|
13
|
-
receiver;
|
|
14
|
-
listeners = new Set();
|
|
15
|
-
constructor(options) {
|
|
16
|
-
this.serviceId = options.serviceId ?? `utils-${randomUUID()}`;
|
|
17
|
-
this.transport = new UnixSocketServerTransport({ ...options, serviceId: this.serviceId });
|
|
18
|
-
this.receiver = this.receive();
|
|
19
|
-
// Startup failure is reported by open(); don't leave a second rejected promise.
|
|
20
|
-
void this.receiver.catch(() => undefined);
|
|
21
|
-
}
|
|
22
|
-
static async open(options = {}) {
|
|
23
|
-
const publisher = new Publisher(options);
|
|
24
|
-
try {
|
|
25
|
-
await publisher.transport.ready;
|
|
26
|
-
return publisher;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
await publisher.close();
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
onAction(listener) {
|
|
34
|
-
this.listeners.add(listener);
|
|
35
|
-
return () => { this.listeners.delete(listener); };
|
|
36
|
-
}
|
|
37
|
-
list() {
|
|
38
|
-
const surfaceIds = this.snapshot.messages().flatMap((message) => "createSurface" in message ? [message.createSurface.surfaceId] : []);
|
|
39
|
-
return { serviceId: this.serviceId, surfaceIds };
|
|
40
|
-
}
|
|
41
|
-
publish(messages) {
|
|
42
|
-
const batch = structuredClone(messages);
|
|
43
|
-
const work = this.queue.then(async () => {
|
|
44
|
-
if (this.closing)
|
|
45
|
-
throw new Error("publisher is closed");
|
|
46
|
-
if (!batch.length)
|
|
47
|
-
throw new Error("messages must not be empty");
|
|
48
|
-
// Validate against a disposable snapshot so a bad batch cannot partly publish.
|
|
49
|
-
const candidate = new A2uiSnapshot();
|
|
50
|
-
for (const message of this.snapshot.messages())
|
|
51
|
-
candidate.apply(message);
|
|
52
|
-
for (const [index, message] of batch.entries()) {
|
|
53
|
-
try {
|
|
54
|
-
assertSafeDataPath(message);
|
|
55
|
-
candidate.apply(message);
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
throw new Error(`messages[${index}]: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
for (const message of batch)
|
|
62
|
-
await this.transport.send(message);
|
|
63
|
-
this.snapshot = candidate;
|
|
64
|
-
return { accepted: batch.length, ...this.list() };
|
|
65
|
-
});
|
|
66
|
-
this.queue = work.catch(() => undefined);
|
|
67
|
-
return work;
|
|
68
|
-
}
|
|
69
|
-
close() {
|
|
70
|
-
if (this.closePromise)
|
|
71
|
-
return this.closePromise;
|
|
72
|
-
this.closing = true;
|
|
73
|
-
this.events.close();
|
|
74
|
-
this.closePromise = (async () => {
|
|
75
|
-
await this.queue;
|
|
76
|
-
await this.transport.close();
|
|
77
|
-
await this.receiver.catch(() => undefined);
|
|
78
|
-
})();
|
|
79
|
-
return this.closePromise;
|
|
80
|
-
}
|
|
81
|
-
async receive() {
|
|
82
|
-
for await (const message of this.transport) {
|
|
83
|
-
if (this.closing)
|
|
84
|
-
break;
|
|
85
|
-
this.events.push(message);
|
|
86
|
-
for (const listener of this.listeners)
|
|
87
|
-
listener(message);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Snapshot traversal must not follow JavaScript prototype properties from raw input.
|
|
92
|
-
function assertSafeDataPath(message) {
|
|
93
|
-
if (!message || typeof message !== "object" || !("updateDataModel" in message))
|
|
94
|
-
return;
|
|
95
|
-
const update = message.updateDataModel;
|
|
96
|
-
if (!update || typeof update !== "object" || !("path" in update) || typeof update.path !== "string")
|
|
97
|
-
return;
|
|
98
|
-
const parts = update.path.split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
99
|
-
if (parts.some((part) => ["__proto__", "constructor", "prototype"].includes(part)))
|
|
100
|
-
throw new Error("unsafe data model path");
|
|
101
|
-
}
|
package/dist/stream.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Readable } from "node:stream";
|
|
2
|
-
import type { UI } from "@omochi-ui/sdk";
|
|
3
|
-
import { Publisher } from "./publisher.js";
|
|
4
|
-
import { UtilitySurface } from "./surface.js";
|
|
5
|
-
export interface StreamOptions {
|
|
6
|
-
name: string;
|
|
7
|
-
input?: Readable;
|
|
8
|
-
}
|
|
9
|
-
export declare class InputStream extends UtilitySurface {
|
|
10
|
-
private readonly output;
|
|
11
|
-
private readonly input;
|
|
12
|
-
private status;
|
|
13
|
-
constructor(publisher: Publisher, options: StreamOptions);
|
|
14
|
-
start(): Promise<void>;
|
|
15
|
-
protected build(ui: UI): void;
|
|
16
|
-
close(): Promise<void>;
|
|
17
|
-
private consume;
|
|
18
|
-
}
|
package/dist/stream.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { OutputBuffer, fenced } from "./output.js";
|
|
2
|
-
import { UtilitySurface } from "./surface.js";
|
|
3
|
-
export class InputStream extends UtilitySurface {
|
|
4
|
-
output = new OutputBuffer();
|
|
5
|
-
input;
|
|
6
|
-
status = "Reading input…";
|
|
7
|
-
constructor(publisher, options) {
|
|
8
|
-
super(publisher, options.name);
|
|
9
|
-
this.input = options.input ?? process.stdin;
|
|
10
|
-
}
|
|
11
|
-
async start() {
|
|
12
|
-
await super.start();
|
|
13
|
-
void this.consume();
|
|
14
|
-
}
|
|
15
|
-
build(ui) {
|
|
16
|
-
ui.text(this.name, { key: "title", variant: "h3" });
|
|
17
|
-
ui.text(this.status, { key: "status" });
|
|
18
|
-
ui.text(fenced(this.output.text, this.output.truncated), { key: "output" });
|
|
19
|
-
}
|
|
20
|
-
close() {
|
|
21
|
-
this.closeSurface();
|
|
22
|
-
this.input.pause();
|
|
23
|
-
return Promise.resolve();
|
|
24
|
-
}
|
|
25
|
-
async consume() {
|
|
26
|
-
this.input.setEncoding("utf8");
|
|
27
|
-
try {
|
|
28
|
-
for await (const chunk of this.input) {
|
|
29
|
-
if (this.closing)
|
|
30
|
-
break;
|
|
31
|
-
this.output.append(String(chunk));
|
|
32
|
-
this.scheduleRender();
|
|
33
|
-
}
|
|
34
|
-
if (!this.closing) {
|
|
35
|
-
this.status = "Input closed";
|
|
36
|
-
this.scheduleRender();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (!this.closing) {
|
|
41
|
-
this.status = `Input error: ${error instanceof Error ? error.message : String(error)}`;
|
|
42
|
-
this.scheduleRender();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
package/dist/surface.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Reconciler, UI } from "@omochi-ui/sdk";
|
|
2
|
-
import { Publisher } from "./publisher.js";
|
|
3
|
-
export declare abstract class UtilitySurface {
|
|
4
|
-
protected readonly publisher: Publisher;
|
|
5
|
-
protected readonly name: string;
|
|
6
|
-
protected readonly reconciler: Reconciler;
|
|
7
|
-
protected closing: boolean;
|
|
8
|
-
private readonly unsubscribe;
|
|
9
|
-
private timer?;
|
|
10
|
-
constructor(publisher: Publisher, name: string);
|
|
11
|
-
start(): Promise<void>;
|
|
12
|
-
protected abstract build(ui: UI): void;
|
|
13
|
-
protected onAction(_name: string): void;
|
|
14
|
-
protected scheduleRender(): void;
|
|
15
|
-
protected render(): Promise<void>;
|
|
16
|
-
protected closeSurface(): void;
|
|
17
|
-
private receiveAction;
|
|
18
|
-
}
|