@omochi-ui/utils 0.0.0-stage → 0.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/LICENSE +202 -0
- package/README.md +201 -2
- package/bin/omochi-utils.js +2 -0
- package/dist/button.d.ts +20 -0
- package/dist/button.js +51 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +239 -0
- package/dist/command.d.ts +28 -0
- package/dist/command.js +110 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.js +54 -0
- package/dist/http-body.d.ts +16 -0
- package/dist/http-body.js +59 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +156 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +59 -0
- package/dist/output.d.ts +10 -0
- package/dist/output.js +35 -0
- package/dist/process.d.ts +32 -0
- package/dist/process.js +115 -0
- package/dist/publisher.d.ts +31 -0
- package/dist/publisher.js +101 -0
- package/dist/stream.d.ts +18 -0
- package/dist/stream.js +46 -0
- package/dist/surface.d.ts +18 -0
- package/dist/surface.js +51 -0
- package/dist/tail.d.ts +25 -0
- package/dist/tail.js +130 -0
- package/dist/watch.d.ts +37 -0
- package/dist/watch.js +130 -0
- package/package.json +35 -4
package/dist/surface.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BASIC_CATALOG, Reconciler, UI } from "@omochi-ui/sdk";
|
|
2
|
+
export class UtilitySurface {
|
|
3
|
+
publisher;
|
|
4
|
+
name;
|
|
5
|
+
reconciler = new Reconciler();
|
|
6
|
+
closing = false;
|
|
7
|
+
unsubscribe;
|
|
8
|
+
timer;
|
|
9
|
+
constructor(publisher, name) {
|
|
10
|
+
this.publisher = publisher;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.unsubscribe = publisher.onAction((message) => this.receiveAction(message));
|
|
13
|
+
}
|
|
14
|
+
async start() { await this.render(); }
|
|
15
|
+
onAction(_name) { }
|
|
16
|
+
scheduleRender() {
|
|
17
|
+
if (this.timer || this.closing)
|
|
18
|
+
return;
|
|
19
|
+
this.timer = setTimeout(() => {
|
|
20
|
+
this.timer = undefined;
|
|
21
|
+
void this.render().catch((error) => {
|
|
22
|
+
if (!this.closing)
|
|
23
|
+
process.stderr.write(`surface render: ${String(error)}\n`);
|
|
24
|
+
});
|
|
25
|
+
}, 50);
|
|
26
|
+
}
|
|
27
|
+
async render() {
|
|
28
|
+
if (this.closing)
|
|
29
|
+
return;
|
|
30
|
+
const ui = new UI("main", BASIC_CATALOG);
|
|
31
|
+
this.build(ui);
|
|
32
|
+
const messages = this.reconciler.render(ui.build());
|
|
33
|
+
if (messages.length)
|
|
34
|
+
await this.publisher.publish(messages);
|
|
35
|
+
}
|
|
36
|
+
closeSurface() {
|
|
37
|
+
if (this.closing)
|
|
38
|
+
return;
|
|
39
|
+
this.closing = true;
|
|
40
|
+
this.unsubscribe();
|
|
41
|
+
clearTimeout(this.timer);
|
|
42
|
+
}
|
|
43
|
+
receiveAction(message) {
|
|
44
|
+
const action = message.action;
|
|
45
|
+
if (!action || typeof action !== "object" || Array.isArray(action))
|
|
46
|
+
return;
|
|
47
|
+
if (action.surfaceId !== "main" || typeof action.name !== "string")
|
|
48
|
+
return;
|
|
49
|
+
this.onAction(action.name);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/tail.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UI } from "@omochi-ui/sdk";
|
|
2
|
+
import { Publisher } from "./publisher.js";
|
|
3
|
+
import { UtilitySurface } from "./surface.js";
|
|
4
|
+
export interface TailOptions {
|
|
5
|
+
path: string;
|
|
6
|
+
lines: number;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class FileTail extends UtilitySurface {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly output;
|
|
12
|
+
private decoder;
|
|
13
|
+
private identity?;
|
|
14
|
+
private position;
|
|
15
|
+
private seenFile;
|
|
16
|
+
private status;
|
|
17
|
+
private pollTimer?;
|
|
18
|
+
constructor(publisher: Publisher, options: TailOptions);
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
protected build(ui: UI): void;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
private poll;
|
|
23
|
+
private readInitial;
|
|
24
|
+
private readRange;
|
|
25
|
+
}
|
package/dist/tail.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { open, stat } from "node:fs/promises";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { OutputBuffer, fenced } from "./output.js";
|
|
4
|
+
import { UtilitySurface } from "./surface.js";
|
|
5
|
+
const READ_LIMIT = 64 * 1024;
|
|
6
|
+
const POLL_INTERVAL = 500;
|
|
7
|
+
export class FileTail extends UtilitySurface {
|
|
8
|
+
options;
|
|
9
|
+
output = new OutputBuffer();
|
|
10
|
+
decoder = new StringDecoder("utf8");
|
|
11
|
+
identity;
|
|
12
|
+
position = 0;
|
|
13
|
+
seenFile = false;
|
|
14
|
+
status = "Waiting for file…";
|
|
15
|
+
pollTimer;
|
|
16
|
+
constructor(publisher, options) {
|
|
17
|
+
super(publisher, options.name);
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
async start() {
|
|
21
|
+
await super.start();
|
|
22
|
+
void this.poll();
|
|
23
|
+
}
|
|
24
|
+
build(ui) {
|
|
25
|
+
ui.text(this.name, { key: "title", variant: "h3" });
|
|
26
|
+
ui.text(this.status, { key: "status" });
|
|
27
|
+
ui.text(fenced(this.output.text, this.output.truncated), { key: "output" });
|
|
28
|
+
}
|
|
29
|
+
close() {
|
|
30
|
+
this.closeSurface();
|
|
31
|
+
clearTimeout(this.pollTimer);
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
async poll() {
|
|
35
|
+
if (this.closing)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const info = await stat(this.options.path);
|
|
39
|
+
if (!info.isFile())
|
|
40
|
+
throw new Error("path is not a regular file");
|
|
41
|
+
const identity = `${info.dev}:${info.ino}`;
|
|
42
|
+
if (identity !== this.identity) {
|
|
43
|
+
if (this.seenFile)
|
|
44
|
+
this.output.append(`\n--- File replaced at ${new Date().toISOString()} ---\n`);
|
|
45
|
+
await this.readInitial(info.size);
|
|
46
|
+
this.identity = identity;
|
|
47
|
+
this.seenFile = true;
|
|
48
|
+
}
|
|
49
|
+
else if (info.size < this.position) {
|
|
50
|
+
this.output.append(`\n--- File truncated at ${new Date().toISOString()} ---\n`);
|
|
51
|
+
await this.readInitial(info.size);
|
|
52
|
+
}
|
|
53
|
+
else if (info.size > this.position) {
|
|
54
|
+
await this.readRange(this.position, info.size);
|
|
55
|
+
}
|
|
56
|
+
this.status = `Following ${this.options.path} · ${new Date().toISOString()}`;
|
|
57
|
+
this.scheduleRender();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const code = error.code;
|
|
61
|
+
if (code === "ENOENT") {
|
|
62
|
+
if (this.identity !== undefined)
|
|
63
|
+
this.output.append(`\n--- File missing; waiting for replacement ---\n`);
|
|
64
|
+
this.identity = undefined;
|
|
65
|
+
this.position = 0;
|
|
66
|
+
this.decoder = new StringDecoder("utf8");
|
|
67
|
+
this.status = `Waiting for ${this.options.path}…`;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.status = `Tail error: ${error instanceof Error ? error.message : String(error)}`;
|
|
71
|
+
}
|
|
72
|
+
this.scheduleRender();
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (!this.closing)
|
|
76
|
+
this.pollTimer = setTimeout(() => void this.poll(), POLL_INTERVAL);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async readInitial(size) {
|
|
80
|
+
this.decoder = new StringDecoder("utf8");
|
|
81
|
+
const start = Math.max(0, size - READ_LIMIT);
|
|
82
|
+
const bytes = await readBytes(this.options.path, start, size - start);
|
|
83
|
+
let offset = 0;
|
|
84
|
+
if (start > 0)
|
|
85
|
+
while (offset < bytes.length && (bytes[offset] & 0xc0) === 0x80)
|
|
86
|
+
offset++;
|
|
87
|
+
const decoded = this.decoder.write(bytes.subarray(offset));
|
|
88
|
+
const lines = decoded.split("\n");
|
|
89
|
+
const selected = lines.slice(Math.max(0, lines.length - this.options.lines - (decoded.endsWith("\n") ? 1 : 0))).join("\n");
|
|
90
|
+
if (!this.seenFile)
|
|
91
|
+
this.output.reset(selected);
|
|
92
|
+
else
|
|
93
|
+
this.output.append(selected);
|
|
94
|
+
this.position = size;
|
|
95
|
+
}
|
|
96
|
+
async readRange(start, end) {
|
|
97
|
+
let effectiveStart = start;
|
|
98
|
+
if (end - start > READ_LIMIT) {
|
|
99
|
+
effectiveStart = end - READ_LIMIT;
|
|
100
|
+
this.output.append("\n--- Earlier appended data skipped ---\n");
|
|
101
|
+
this.decoder = new StringDecoder("utf8");
|
|
102
|
+
}
|
|
103
|
+
const bytes = await readBytes(this.options.path, effectiveStart, end - effectiveStart);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
if (effectiveStart !== start)
|
|
106
|
+
while (offset < bytes.length && (bytes[offset] & 0xc0) === 0x80)
|
|
107
|
+
offset++;
|
|
108
|
+
this.output.append(this.decoder.write(bytes.subarray(offset)));
|
|
109
|
+
this.position = end;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readBytes(filePath, position, length) {
|
|
113
|
+
if (length <= 0)
|
|
114
|
+
return Buffer.alloc(0);
|
|
115
|
+
const handle = await open(filePath, "r");
|
|
116
|
+
try {
|
|
117
|
+
const buffer = Buffer.alloc(length);
|
|
118
|
+
let offset = 0;
|
|
119
|
+
while (offset < length) {
|
|
120
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
121
|
+
if (!bytesRead)
|
|
122
|
+
break;
|
|
123
|
+
offset += bytesRead;
|
|
124
|
+
}
|
|
125
|
+
return buffer.subarray(0, offset);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await handle.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type UI } from "@omochi-ui/sdk";
|
|
2
|
+
import { Publisher } from "./publisher.js";
|
|
3
|
+
import { UtilitySurface } from "./surface.js";
|
|
4
|
+
export interface WatchOptions {
|
|
5
|
+
command: string[];
|
|
6
|
+
cwd?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
intervalMs: number;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
diff: boolean;
|
|
11
|
+
onChange: boolean;
|
|
12
|
+
history?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class CommandWatch extends UtilitySurface {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly current;
|
|
17
|
+
private readonly runner;
|
|
18
|
+
private readonly entries;
|
|
19
|
+
private interval?;
|
|
20
|
+
private running;
|
|
21
|
+
private paused;
|
|
22
|
+
private status;
|
|
23
|
+
private displayedOutput;
|
|
24
|
+
private displayedTruncated;
|
|
25
|
+
private difference;
|
|
26
|
+
private differencePlaceholder;
|
|
27
|
+
private differenceTruncated;
|
|
28
|
+
private previousOutput?;
|
|
29
|
+
private previousKey?;
|
|
30
|
+
private closePromise?;
|
|
31
|
+
constructor(publisher: Publisher, options: WatchOptions);
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
protected build(ui: UI): void;
|
|
34
|
+
protected onAction(name: string): void;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
private run;
|
|
37
|
+
}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Action } from "@omochi-ui/sdk";
|
|
2
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
|
+
import { ManagedCommand, describeResult } from "./command.js";
|
|
4
|
+
import { OutputBuffer, fenced, formatDuration } from "./output.js";
|
|
5
|
+
import { UtilitySurface } from "./surface.js";
|
|
6
|
+
export class CommandWatch extends UtilitySurface {
|
|
7
|
+
options;
|
|
8
|
+
current = new OutputBuffer();
|
|
9
|
+
runner;
|
|
10
|
+
entries = [];
|
|
11
|
+
interval;
|
|
12
|
+
running = false;
|
|
13
|
+
paused = false;
|
|
14
|
+
status = "Starting…";
|
|
15
|
+
displayedOutput = "";
|
|
16
|
+
displayedTruncated = false;
|
|
17
|
+
difference = "";
|
|
18
|
+
differencePlaceholder = "No previous run.";
|
|
19
|
+
differenceTruncated = false;
|
|
20
|
+
previousOutput;
|
|
21
|
+
previousKey;
|
|
22
|
+
closePromise;
|
|
23
|
+
constructor(publisher, options) {
|
|
24
|
+
super(publisher, options.name);
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.runner = new ManagedCommand({
|
|
27
|
+
command: options.command,
|
|
28
|
+
cwd: options.cwd,
|
|
29
|
+
onData: (chunk) => {
|
|
30
|
+
this.current.append(chunk);
|
|
31
|
+
if (!options.onChange) {
|
|
32
|
+
this.displayedOutput = this.current.text;
|
|
33
|
+
this.displayedTruncated = this.current.truncated;
|
|
34
|
+
this.scheduleRender();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
await super.start();
|
|
41
|
+
void this.run();
|
|
42
|
+
}
|
|
43
|
+
build(ui) {
|
|
44
|
+
ui.text(this.name, { key: "title", variant: "h3" });
|
|
45
|
+
ui.row({ key: "controls" }, () => {
|
|
46
|
+
if (this.paused)
|
|
47
|
+
ui.button("Resume", new Action("resume").call(null), { key: "resume", variant: "primary" });
|
|
48
|
+
else
|
|
49
|
+
ui.button("Pause", new Action("pause").call(null), { key: "pause" });
|
|
50
|
+
if (!this.running)
|
|
51
|
+
ui.button("Run now", new Action("run").call(null), { key: "run" });
|
|
52
|
+
});
|
|
53
|
+
ui.text(`${this.status}${this.paused ? " · Paused" : ""}`, { key: "status" });
|
|
54
|
+
ui.text(fenced(this.displayedOutput, this.displayedTruncated), { key: "output" });
|
|
55
|
+
if (this.options.diff) {
|
|
56
|
+
ui.text("Changes since previous run", { key: "diff-title", variant: "h4" });
|
|
57
|
+
ui.text(fenced(this.difference || this.differencePlaceholder, this.differenceTruncated), { key: "diff" });
|
|
58
|
+
}
|
|
59
|
+
if (this.options.history && this.entries.length) {
|
|
60
|
+
ui.text("History", { key: "history-title", variant: "h4" });
|
|
61
|
+
ui.text(this.entries.map((entry) => `- ${entry.at} · ${entry.result} · ${formatDuration(entry.durationMs)} · ${entry.changed ? "changed" : "unchanged"}`).join("\n"), { key: "history" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
onAction(name) {
|
|
65
|
+
if (name === "pause" && !this.paused) {
|
|
66
|
+
this.paused = true;
|
|
67
|
+
clearTimeout(this.interval);
|
|
68
|
+
this.scheduleRender();
|
|
69
|
+
}
|
|
70
|
+
else if (name === "resume" && this.paused) {
|
|
71
|
+
this.paused = false;
|
|
72
|
+
this.scheduleRender();
|
|
73
|
+
if (!this.running)
|
|
74
|
+
void this.run();
|
|
75
|
+
}
|
|
76
|
+
else if (name === "run" && !this.running) {
|
|
77
|
+
clearTimeout(this.interval);
|
|
78
|
+
void this.run();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
close() {
|
|
82
|
+
if (this.closePromise)
|
|
83
|
+
return this.closePromise;
|
|
84
|
+
this.closeSurface();
|
|
85
|
+
clearTimeout(this.interval);
|
|
86
|
+
this.closePromise = this.runner.close();
|
|
87
|
+
return this.closePromise;
|
|
88
|
+
}
|
|
89
|
+
async run() {
|
|
90
|
+
if (this.running || this.closing)
|
|
91
|
+
return;
|
|
92
|
+
this.running = true;
|
|
93
|
+
this.current.reset();
|
|
94
|
+
if (!this.options.onChange) {
|
|
95
|
+
this.status = "Running…";
|
|
96
|
+
this.displayedOutput = "";
|
|
97
|
+
this.displayedTruncated = false;
|
|
98
|
+
this.scheduleRender();
|
|
99
|
+
}
|
|
100
|
+
const result = await this.runner.start(this.options.timeoutMs);
|
|
101
|
+
const output = this.current.text;
|
|
102
|
+
const key = `${resultKey(result)}\0${output}`;
|
|
103
|
+
const changed = this.previousKey === undefined || key !== this.previousKey;
|
|
104
|
+
const difference = new OutputBuffer();
|
|
105
|
+
const hadPrevious = this.previousOutput !== undefined;
|
|
106
|
+
difference.reset(hadPrevious ? createTwoFilesPatch("previous", "current", this.previousOutput, output, "", "", { context: 3 }) : "");
|
|
107
|
+
this.difference = difference.text;
|
|
108
|
+
this.differenceTruncated = difference.truncated;
|
|
109
|
+
this.differencePlaceholder = hadPrevious ? "No changes." : "No previous run.";
|
|
110
|
+
this.previousOutput = output;
|
|
111
|
+
this.previousKey = key;
|
|
112
|
+
this.running = false;
|
|
113
|
+
this.status = `${describeResult(result)} · ${formatDuration(result.durationMs)} · ${new Date().toISOString()}`;
|
|
114
|
+
if (this.options.history !== undefined && this.options.history > 0) {
|
|
115
|
+
this.entries.push({ at: new Date().toISOString(), durationMs: result.durationMs, result: describeResult(result), changed });
|
|
116
|
+
if (this.entries.length > this.options.history)
|
|
117
|
+
this.entries.splice(0, this.entries.length - this.options.history);
|
|
118
|
+
}
|
|
119
|
+
if (!this.options.onChange || changed) {
|
|
120
|
+
this.displayedOutput = output;
|
|
121
|
+
this.displayedTruncated = this.current.truncated;
|
|
122
|
+
this.scheduleRender();
|
|
123
|
+
}
|
|
124
|
+
if (!this.paused && !this.closing)
|
|
125
|
+
this.interval = setTimeout(() => void this.run(), this.options.intervalMs);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function resultKey(result) {
|
|
129
|
+
return result.error ? `error:${result.error.message}` : result.timedOut ? "timeout" : result.signal ? `signal:${result.signal}` : `code:${result.code}`;
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omochi-ui/utils",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"bun": ">=1.4.3"
|
|
10
|
+
},
|
|
11
|
+
"description": "Publish development utilities and agent-created A2UI surfaces to Omochi",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"omochi-utils": "./bin/omochi-utils.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
23
|
+
"diff": "^9.0.0",
|
|
24
|
+
"@omochi-ui/core": "0.1.0",
|
|
25
|
+
"@omochi-ui/sdk": "0.1.0",
|
|
26
|
+
"zod": "^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/k-kozika/omochi.git",
|
|
31
|
+
"directory": "packages/utils"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/k-kozika/omochi#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/k-kozika/omochi/issues"
|
|
36
|
+
}
|
|
37
|
+
}
|