@pinixai/core 0.1.0 → 0.3.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/package.json +1 -1
- package/src/clip.ts +19 -0
- package/src/http.ts +253 -0
- package/src/index.ts +2 -1
- package/src/ipc.ts +123 -122
- package/src/manifest.ts +20 -0
package/package.json
CHANGED
package/src/clip.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CLIHelpError, formatCLIHelp, parseCLIArgs } from "./cli";
|
|
3
3
|
import type { HandlerDef } from "./handler";
|
|
4
|
+
import { serveHTTP } from "./http";
|
|
4
5
|
import { serveIPC } from "./ipc";
|
|
5
6
|
import { serveMCP } from "./mcp";
|
|
6
7
|
import { generateManifest } from "./manifest";
|
|
@@ -44,6 +45,23 @@ export abstract class Clip {
|
|
|
44
45
|
return serveIPC(this);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
if (modeOrCommand === "--web") {
|
|
49
|
+
const portArg = restArgs[0];
|
|
50
|
+
|
|
51
|
+
if (portArg === undefined) {
|
|
52
|
+
return serveHTTP(this);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const port = Number.parseInt(portArg, 10);
|
|
56
|
+
|
|
57
|
+
if (Number.isNaN(port)) {
|
|
58
|
+
console.error(`Invalid port: ${portArg}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return serveHTTP(this, port);
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
if (modeOrCommand === "--manifest") {
|
|
48
66
|
console.log(this.toManifest());
|
|
49
67
|
return;
|
|
@@ -102,6 +120,7 @@ export abstract class Clip {
|
|
|
102
120
|
lines.push(" bun run <script> --manifest");
|
|
103
121
|
lines.push(" bun run <script> --mcp");
|
|
104
122
|
lines.push(" bun run <script> --ipc");
|
|
123
|
+
lines.push(" bun run <script> --web [port]");
|
|
105
124
|
lines.push("");
|
|
106
125
|
lines.push("Commands:");
|
|
107
126
|
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
2
|
+
import type { Clip } from "./clip";
|
|
3
|
+
import { zodToManifestType } from "./manifest";
|
|
4
|
+
|
|
5
|
+
const CORS_HEADERS = {
|
|
6
|
+
"Access-Control-Allow-Origin": "*",
|
|
7
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
8
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function toErrorMessage(error: unknown): string {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createHeaders(headers?: Headers | Record<string, string>): Headers {
|
|
16
|
+
const result = new Headers(CORS_HEADERS);
|
|
17
|
+
|
|
18
|
+
if (!headers) {
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const extraHeaders = new Headers(headers);
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of extraHeaders.entries()) {
|
|
25
|
+
result.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
32
|
+
return new Response(JSON.stringify(body), {
|
|
33
|
+
status,
|
|
34
|
+
headers: createHeaders({
|
|
35
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function textResponse(body: string, status = 200): Response {
|
|
41
|
+
return new Response(body, {
|
|
42
|
+
status,
|
|
43
|
+
headers: createHeaders({
|
|
44
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function errorResponse(message: string, status: number): Response {
|
|
50
|
+
return jsonResponse({ error: message }, status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveWithinRoot(root: string, relativePath: string): string | null {
|
|
54
|
+
const candidate = resolve(root, relativePath);
|
|
55
|
+
|
|
56
|
+
if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getStaticRoots(): string[] {
|
|
64
|
+
const scriptDir = dirname(Bun.main);
|
|
65
|
+
return [join(scriptDir, "web", "dist"), join(scriptDir, "web")];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeStaticPath(pathname: string): string {
|
|
69
|
+
const decodedPathname = decodeURIComponent(pathname);
|
|
70
|
+
const trimmedPathname = decodedPathname.replace(/^\/+/, "");
|
|
71
|
+
|
|
72
|
+
if (trimmedPathname.length === 0) {
|
|
73
|
+
return "index.html";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (decodedPathname.endsWith("/")) {
|
|
77
|
+
return join(trimmedPathname, "index.html");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return trimmedPathname;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function findStaticFile(pathname: string): Promise<ReturnType<typeof Bun.file> | null> {
|
|
84
|
+
const relativePath = normalizeStaticPath(pathname);
|
|
85
|
+
|
|
86
|
+
for (const root of getStaticRoots()) {
|
|
87
|
+
const candidatePath = resolveWithinRoot(root, relativePath);
|
|
88
|
+
|
|
89
|
+
if (!candidatePath) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const file = Bun.file(candidatePath);
|
|
94
|
+
|
|
95
|
+
if (await file.exists()) {
|
|
96
|
+
return file;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fileResponse(file: ReturnType<typeof Bun.file>): Response {
|
|
104
|
+
const headers = file.type
|
|
105
|
+
? createHeaders({ "Content-Type": file.type })
|
|
106
|
+
: createHeaders();
|
|
107
|
+
|
|
108
|
+
return new Response(file, { headers });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readJSONBody(request: Request): Promise<unknown> {
|
|
112
|
+
const bodyText = await request.text();
|
|
113
|
+
|
|
114
|
+
if (bodyText.trim().length === 0) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(bodyText) as unknown;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new Error(`Invalid JSON: ${toErrorMessage(error)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function listCommands(clip: Clip): Response {
|
|
126
|
+
const commands = Array.from(clip.getCommands().entries()).map(([name, commandHandler]) => ({
|
|
127
|
+
name,
|
|
128
|
+
description: clip.getCommandDescription(name) ?? null,
|
|
129
|
+
method: "POST",
|
|
130
|
+
path: `/api/${name}`,
|
|
131
|
+
input: zodToManifestType(commandHandler.input),
|
|
132
|
+
output: zodToManifestType(commandHandler.output),
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
return jsonResponse({ commands });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function handleCommandRequest(clip: Clip, commandName: string, request: Request): Promise<Response> {
|
|
139
|
+
const commandHandler = clip.getCommands().get(commandName);
|
|
140
|
+
|
|
141
|
+
if (!commandHandler) {
|
|
142
|
+
return errorResponse(`Unknown command: ${commandName}`, 404);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let input: unknown;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
input = await readJSONBody(request);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return errorResponse(toErrorMessage(error), 400);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let parsedInput: unknown;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
parsedInput = await commandHandler.input.parseAsync(input);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return errorResponse(toErrorMessage(error), 400);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let output: unknown;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
output = await commandHandler.fn(parsedInput as never);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return errorResponse(toErrorMessage(error), 500);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const parsedOutput = await commandHandler.output.parseAsync(output);
|
|
171
|
+
return jsonResponse(parsedOutput);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return errorResponse(toErrorMessage(error), 500);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function handleStaticRequest(pathname: string): Promise<Response> {
|
|
178
|
+
const file = await findStaticFile(pathname);
|
|
179
|
+
|
|
180
|
+
if (file) {
|
|
181
|
+
return fileResponse(file);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const fallbackFile = await findStaticFile("/index.html");
|
|
185
|
+
|
|
186
|
+
if (fallbackFile) {
|
|
187
|
+
return fileResponse(fallbackFile);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return errorResponse(`Static file not found: ${pathname}`, 404);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function handleRequest(clip: Clip, request: Request): Promise<Response> {
|
|
194
|
+
const url = new URL(request.url);
|
|
195
|
+
const pathname = url.pathname;
|
|
196
|
+
const method = request.method.toUpperCase();
|
|
197
|
+
|
|
198
|
+
if (method === "OPTIONS") {
|
|
199
|
+
return new Response(null, {
|
|
200
|
+
status: 204,
|
|
201
|
+
headers: createHeaders(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (pathname === "/manifest") {
|
|
206
|
+
if (method !== "GET") {
|
|
207
|
+
return errorResponse("Method not allowed", 405);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return textResponse(clip.toManifest());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (pathname === "/api" || pathname === "/api/") {
|
|
214
|
+
if (method !== "GET") {
|
|
215
|
+
return errorResponse("Method not allowed", 405);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return listCommands(clip);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (pathname.startsWith("/api/")) {
|
|
222
|
+
if (method !== "POST") {
|
|
223
|
+
return errorResponse("Method not allowed", 405);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const commandName = pathname.slice("/api/".length);
|
|
227
|
+
|
|
228
|
+
if (commandName.length === 0 || commandName.includes("/")) {
|
|
229
|
+
return errorResponse("Unknown command", 404);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return handleCommandRequest(clip, commandName, request);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
236
|
+
return errorResponse("Method not allowed", 405);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
return await handleStaticRequest(pathname);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return errorResponse(toErrorMessage(error), 500);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function serveHTTP(clip: Clip, port = 3000): Promise<void> {
|
|
247
|
+
const server = Bun.serve({
|
|
248
|
+
port,
|
|
249
|
+
fetch: (request) => handleRequest(clip, request),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
console.error(`Clip "${clip.name}" running at http://localhost:${server.port}`);
|
|
253
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Clip } from "./clip";
|
|
2
2
|
export { command } from "./command";
|
|
3
3
|
export { handler, type HandlerDef } from "./handler";
|
|
4
|
-
export {
|
|
4
|
+
export { serveHTTP } from "./http";
|
|
5
|
+
export { serveIPC, invoke } from "./ipc";
|
|
5
6
|
export { serveMCP } from "./mcp";
|
|
6
7
|
export { z } from "zod";
|
package/src/ipc.ts
CHANGED
|
@@ -1,160 +1,161 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
1
|
import type { Clip } from "./clip";
|
|
2
|
+
import { createIPCManifest } from "./manifest";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
id?: unknown;
|
|
6
|
-
command?: unknown;
|
|
7
|
-
input?: unknown;
|
|
8
|
-
};
|
|
4
|
+
// === IPC Protocol Types ===
|
|
9
5
|
|
|
10
|
-
type
|
|
11
|
-
| {
|
|
12
|
-
id: unknown;
|
|
13
|
-
output: unknown;
|
|
14
|
-
}
|
|
15
|
-
| {
|
|
16
|
-
id: unknown;
|
|
17
|
-
error: {
|
|
18
|
-
message: string;
|
|
19
|
-
code: string;
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function toErrorMessage(error: unknown): string {
|
|
24
|
-
return error instanceof Error ? error.message : String(error);
|
|
25
|
-
}
|
|
6
|
+
type MessageType = "register" | "registered" | "invoke" | "result" | "error" | "chunk" | "done";
|
|
26
7
|
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
interface BaseMessage {
|
|
9
|
+
type: MessageType;
|
|
10
|
+
id?: string;
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
interface RegisterMessage extends BaseMessage {
|
|
14
|
+
type: "register";
|
|
15
|
+
manifest: { name: string; domain: string; commands: string[]; dependencies: string[] };
|
|
33
16
|
}
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
};
|
|
18
|
+
interface InvokeMessage extends BaseMessage {
|
|
19
|
+
type: "invoke";
|
|
20
|
+
id: string;
|
|
21
|
+
command?: string;
|
|
22
|
+
clip?: string;
|
|
23
|
+
input?: unknown;
|
|
43
24
|
}
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
interface ResultMessage extends BaseMessage {
|
|
27
|
+
type: "result";
|
|
28
|
+
id: string;
|
|
29
|
+
output: unknown;
|
|
30
|
+
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
32
|
+
interface ErrorMessage extends BaseMessage {
|
|
33
|
+
type: "error";
|
|
34
|
+
id: string;
|
|
35
|
+
error: string;
|
|
36
|
+
}
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
// === State ===
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
40
|
+
const pendingInvokes = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
41
|
+
let idCounter = 0;
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
function nextId(): string {
|
|
44
|
+
return `c${++idCounter}`;
|
|
45
|
+
}
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
47
|
+
function send(msg: Record<string, unknown>): void {
|
|
48
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
49
|
+
}
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
writeResponse(createErrorResponse(id, "Request input must be an object", "INVALID_INPUT"));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
51
|
+
// === Public API ===
|
|
73
52
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
id,
|
|
81
|
-
output: parsedOutput,
|
|
82
|
-
});
|
|
83
|
-
} catch (error) {
|
|
84
|
-
writeResponse(createErrorResponse(id, toErrorMessage(error), "COMMAND_ERROR"));
|
|
85
|
-
}
|
|
53
|
+
export async function invoke(clip: string, command: string, input: unknown): Promise<unknown> {
|
|
54
|
+
const id = nextId();
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
pendingInvokes.set(id, { resolve, reject });
|
|
57
|
+
send({ id, type: "invoke", clip, command, input });
|
|
58
|
+
});
|
|
86
59
|
}
|
|
87
60
|
|
|
88
|
-
|
|
89
|
-
console.log = (...args: unknown[]) => {
|
|
90
|
-
const serialized = args
|
|
91
|
-
.map((arg) => {
|
|
92
|
-
if (typeof arg === "string") {
|
|
93
|
-
return arg;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
return JSON.stringify(arg);
|
|
98
|
-
} catch {
|
|
99
|
-
return String(arg);
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
.join(" ");
|
|
61
|
+
// === IPC Server ===
|
|
103
62
|
|
|
104
|
-
|
|
63
|
+
export async function serveIPC(clip: Clip): Promise<void> {
|
|
64
|
+
// Redirect console.log to stderr so stdout is reserved for IPC
|
|
65
|
+
const origLog = console.log;
|
|
66
|
+
console.log = (...args: unknown[]) => {
|
|
67
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
105
68
|
};
|
|
106
|
-
}
|
|
107
69
|
|
|
108
|
-
|
|
109
|
-
|
|
70
|
+
// Register with pinixd
|
|
71
|
+
const manifest = createIPCManifest(clip);
|
|
72
|
+
send({ type: "register", manifest });
|
|
73
|
+
|
|
74
|
+
// Read messages from stdin
|
|
75
|
+
const reader = createLineReader(process.stdin);
|
|
76
|
+
const commands = clip.getCommands();
|
|
77
|
+
|
|
78
|
+
for await (const line of reader) {
|
|
79
|
+
let msg: BaseMessage;
|
|
80
|
+
try {
|
|
81
|
+
msg = JSON.parse(line) as BaseMessage;
|
|
82
|
+
} catch {
|
|
83
|
+
process.stderr.write(`[ipc] invalid JSON: ${line}\n`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
87
|
+
if (!msg.type) {
|
|
88
|
+
process.stderr.write(`[ipc] message missing type: ${line}\n`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
114
91
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
92
|
+
switch (msg.type) {
|
|
93
|
+
case "registered":
|
|
94
|
+
// Registration confirmed
|
|
95
|
+
break;
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
case "invoke": {
|
|
98
|
+
const inv = msg as InvokeMessage;
|
|
99
|
+
handleInvoke(inv, commands);
|
|
120
100
|
break;
|
|
121
101
|
}
|
|
122
102
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const line = buffer.slice(0, newlineIndex).trim();
|
|
133
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
134
|
-
|
|
135
|
-
if (line.length === 0) {
|
|
136
|
-
continue;
|
|
103
|
+
case "result": {
|
|
104
|
+
const res = msg as ResultMessage;
|
|
105
|
+
const pending = pendingInvokes.get(res.id);
|
|
106
|
+
if (pending) {
|
|
107
|
+
pendingInvokes.delete(res.id);
|
|
108
|
+
pending.resolve(res.output);
|
|
137
109
|
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
113
|
+
case "error": {
|
|
114
|
+
const err = msg as ErrorMessage;
|
|
115
|
+
const pending = pendingInvokes.get(err.id);
|
|
116
|
+
if (pending) {
|
|
117
|
+
pendingInvokes.delete(err.id);
|
|
118
|
+
pending.reject(new Error(err.error));
|
|
143
119
|
}
|
|
120
|
+
break;
|
|
144
121
|
}
|
|
122
|
+
|
|
123
|
+
default:
|
|
124
|
+
process.stderr.write(`[ipc] unknown message type: ${msg.type}\n`);
|
|
145
125
|
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
146
128
|
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
async function handleInvoke(
|
|
130
|
+
msg: InvokeMessage,
|
|
131
|
+
commands: ReturnType<Clip["getCommands"]>,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const cmd = commands.get(msg.command ?? "");
|
|
134
|
+
if (!cmd) {
|
|
135
|
+
send({ id: msg.id, type: "error", error: `unknown command: ${msg.command}` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
139
|
+
try {
|
|
140
|
+
const parsed = cmd.input.parse(msg.input ?? {});
|
|
141
|
+
const output = await cmd.fn(parsed);
|
|
142
|
+
send({ id: msg.id, type: "result", output });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
send({ id: msg.id, type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// === Line Reader ===
|
|
149
|
+
|
|
150
|
+
async function* createLineReader(stream: NodeJS.ReadableStream): AsyncGenerator<string> {
|
|
151
|
+
let buffer = "";
|
|
152
|
+
for await (const chunk of stream) {
|
|
153
|
+
buffer += chunk.toString();
|
|
154
|
+
const lines = buffer.split("\n");
|
|
155
|
+
buffer = lines.pop() ?? "";
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (line.trim()) yield line;
|
|
156
158
|
}
|
|
157
|
-
} finally {
|
|
158
|
-
reader.releaseLock();
|
|
159
159
|
}
|
|
160
|
+
if (buffer.trim()) yield buffer;
|
|
160
161
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z, type ZodType } from "zod";
|
|
2
2
|
import type { Clip } from "./clip";
|
|
3
3
|
|
|
4
|
+
export interface IPCManifest {
|
|
5
|
+
name: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
commands: string[];
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
function formatLiteralValue(value: unknown): string {
|
|
5
12
|
return JSON.stringify(value);
|
|
6
13
|
}
|
|
@@ -139,3 +146,16 @@ export function generateManifest(clip: Clip): string {
|
|
|
139
146
|
|
|
140
147
|
return lines.join("\n");
|
|
141
148
|
}
|
|
149
|
+
|
|
150
|
+
export function createIPCManifest(clip: Clip): IPCManifest {
|
|
151
|
+
const dependencies = (clip as Clip & { dependencies?: unknown }).dependencies;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: clip.name,
|
|
155
|
+
domain: clip.domain,
|
|
156
|
+
commands: Array.from(clip.getCommands().keys()),
|
|
157
|
+
dependencies: Array.isArray(dependencies)
|
|
158
|
+
? dependencies.filter((dependency): dependency is string => typeof dependency === "string")
|
|
159
|
+
: [],
|
|
160
|
+
};
|
|
161
|
+
}
|