@runneth/cli 0.0.0-sha.19a36f654ef6.production
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 +144 -0
- package/dist/build-defaults.d.ts +1 -0
- package/dist/build-defaults.js +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1112 -0
- package/dist/copy.d.ts +25 -0
- package/dist/copy.js +492 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +547 -0
- package/dist/oauth.d.ts +80 -0
- package/dist/oauth.js +592 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +25 -0
- package/dist/skills.d.ts +15 -0
- package/dist/skills.js +89 -0
- package/dist/ssh-stdio.d.ts +63 -0
- package/dist/ssh-stdio.js +608 -0
- package/dist/ssh.d.ts +129 -0
- package/dist/ssh.js +835 -0
- package/package.json +38 -0
- package/skills/runneth/SKILL.md +177 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
export { assertRunnethVmCopyTargetsUseSameResource, runRunnethVmCopy, } from "./copy.js";
|
|
8
|
+
export { getOAuthAccessToken, loginWithOAuth, logoutOAuthCredential, readOAuthCredential, readOAuthCredentialStatus, resolveOAuthCredentialPath, } from "./oauth.js";
|
|
9
|
+
export { resolveRunnethCliHome, resolveSocketPath } from "./paths.js";
|
|
10
|
+
export { installRunnethSshAccess, normalizeRunnethSshTargetName, readRunnethSshTargetStore, removeRunnethSshTarget, resolveRunnethSshTarget, resolveRunnethSshAppUrl, resolveRunnethSshSharedKeyPaths, resolveRunnethSshTargetsPath, resolveRunnethSshTargetPaths, runOpenSsh, runRunnethSshProxy, saveRunnethSshTarget, setDefaultRunnethSshTarget, } from "./ssh.js";
|
|
11
|
+
export { installRunnethSkills, resolveBundledRunnethSkillPath, } from "./skills.js";
|
|
12
|
+
const DEFAULT_SESSION_NAME = "default";
|
|
13
|
+
const MAX_REQUEST_BYTES = 2_000_000;
|
|
14
|
+
const MAX_IDLE_BUFFER_BYTES = 2_000_000;
|
|
15
|
+
const SESSION_NAME_PATTERN = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
16
|
+
const SOCKET_CONNECT_TIMEOUT_MS = 500;
|
|
17
|
+
const DAEMON_START_TIMEOUT_MS = 5_000;
|
|
18
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
|
|
19
|
+
const nowIso = () => {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
};
|
|
22
|
+
const isRecord = (value) => {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
};
|
|
25
|
+
const isErrnoCode = (error, code) => {
|
|
26
|
+
return (typeof error === "object" &&
|
|
27
|
+
error !== null &&
|
|
28
|
+
"code" in error &&
|
|
29
|
+
error.code === code);
|
|
30
|
+
};
|
|
31
|
+
const errorMessage = (error) => {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
};
|
|
34
|
+
export const normalizeSessionName = (value) => {
|
|
35
|
+
const name = value?.trim() || DEFAULT_SESSION_NAME;
|
|
36
|
+
if (!SESSION_NAME_PATTERN.test(name)) {
|
|
37
|
+
throw new Error(`Invalid session name: ${name}. Use letters, numbers, dot, underscore, and dash only.`);
|
|
38
|
+
}
|
|
39
|
+
return name;
|
|
40
|
+
};
|
|
41
|
+
const parseString = (record, key) => {
|
|
42
|
+
const value = record[key];
|
|
43
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
44
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
const parseOptionalString = (record, key) => {
|
|
49
|
+
const value = record[key];
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
54
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
export const parseRunnethCliRequest = (value) => {
|
|
59
|
+
if (!isRecord(value)) {
|
|
60
|
+
throw new Error("Request must be a JSON object");
|
|
61
|
+
}
|
|
62
|
+
const op = parseString(value, "op");
|
|
63
|
+
switch (op) {
|
|
64
|
+
case "open": {
|
|
65
|
+
return {
|
|
66
|
+
cwd: parseString(value, "cwd"),
|
|
67
|
+
name: normalizeSessionName(parseString(value, "name")),
|
|
68
|
+
op,
|
|
69
|
+
...(parseOptionalString(value, "shell")
|
|
70
|
+
? { shell: parseOptionalString(value, "shell") }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
case "send": {
|
|
75
|
+
return {
|
|
76
|
+
command: parseString(value, "command"),
|
|
77
|
+
cwd: parseString(value, "cwd"),
|
|
78
|
+
name: normalizeSessionName(parseString(value, "name")),
|
|
79
|
+
op,
|
|
80
|
+
...(parseOptionalString(value, "shell")
|
|
81
|
+
? { shell: parseOptionalString(value, "shell") }
|
|
82
|
+
: {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case "status":
|
|
86
|
+
case "close": {
|
|
87
|
+
return {
|
|
88
|
+
name: normalizeSessionName(parseString(value, "name")),
|
|
89
|
+
op,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case "list":
|
|
93
|
+
case "ping":
|
|
94
|
+
case "shutdown": {
|
|
95
|
+
return { op };
|
|
96
|
+
}
|
|
97
|
+
default: {
|
|
98
|
+
throw new Error(`Unknown operation: ${op}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const defaultShell = () => {
|
|
103
|
+
const configuredShell = process.env.RUNNETH_CLI_SHELL?.trim() || process.env.SHELL?.trim();
|
|
104
|
+
return configuredShell && configuredShell.length > 0
|
|
105
|
+
? configuredShell
|
|
106
|
+
: "/bin/sh";
|
|
107
|
+
};
|
|
108
|
+
const appendBuffer = (current, chunk) => {
|
|
109
|
+
const next = Buffer.concat([current, chunk]);
|
|
110
|
+
if (next.length <= MAX_IDLE_BUFFER_BYTES) {
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
return next.subarray(next.length - MAX_IDLE_BUFFER_BYTES);
|
|
114
|
+
};
|
|
115
|
+
const notifyWaiters = (session) => {
|
|
116
|
+
const waiters = [...session.waiters];
|
|
117
|
+
session.waiters.clear();
|
|
118
|
+
for (const waiter of waiters) {
|
|
119
|
+
waiter();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const waitForSessionChange = async (session) => {
|
|
123
|
+
if (session.closed) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
session.waiters.add(resolve);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const toPublicSession = (session) => {
|
|
131
|
+
return {
|
|
132
|
+
busy: session.busy,
|
|
133
|
+
cwd: session.cwd,
|
|
134
|
+
name: session.name,
|
|
135
|
+
pid: session.child.pid ?? 0,
|
|
136
|
+
running: !session.closed,
|
|
137
|
+
sessionId: session.sessionId,
|
|
138
|
+
shell: session.shell,
|
|
139
|
+
startedAt: session.startedAt,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
export const createCommandEnvelope = (command) => {
|
|
143
|
+
const marker = `RUNNETH_CLI_${randomUUID().replace(/-/gu, "")}`;
|
|
144
|
+
return {
|
|
145
|
+
input: [
|
|
146
|
+
`printf '\\n${marker}:start\\n'`,
|
|
147
|
+
command,
|
|
148
|
+
"__runneth_cli_status=$?",
|
|
149
|
+
`printf '\\n${marker}:end:%s\\n' "$__runneth_cli_status"`,
|
|
150
|
+
"",
|
|
151
|
+
].join("\n"),
|
|
152
|
+
marker,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
export const extractCommandResult = (input) => {
|
|
156
|
+
const text = input.stdout.toString("utf8");
|
|
157
|
+
const startToken = `\n${input.marker}:start\n`;
|
|
158
|
+
const startIndex = text.indexOf(startToken);
|
|
159
|
+
if (startIndex === -1) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const contentStartIndex = startIndex + startToken.length;
|
|
163
|
+
const endPrefix = `\n${input.marker}:end:`;
|
|
164
|
+
const endIndex = text.indexOf(endPrefix, contentStartIndex);
|
|
165
|
+
if (endIndex === -1) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const exitCodeStartIndex = endIndex + endPrefix.length;
|
|
169
|
+
const endLineIndex = text.indexOf("\n", exitCodeStartIndex);
|
|
170
|
+
if (endLineIndex === -1) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const rawExitCode = text.slice(exitCodeStartIndex, endLineIndex);
|
|
174
|
+
if (!/^\d+$/u.test(rawExitCode)) {
|
|
175
|
+
throw new Error(`Invalid runneth-cli marker exit code: ${rawExitCode}`);
|
|
176
|
+
}
|
|
177
|
+
const contentStartBytes = Buffer.byteLength(text.slice(0, contentStartIndex), "utf8");
|
|
178
|
+
const contentEndBytes = Buffer.byteLength(text.slice(0, endIndex), "utf8");
|
|
179
|
+
const consumedStdoutBytes = Buffer.byteLength(text.slice(0, endLineIndex + 1), "utf8");
|
|
180
|
+
return {
|
|
181
|
+
consumedStdoutBytes,
|
|
182
|
+
exitCode: Number(rawExitCode),
|
|
183
|
+
stdout: input.stdout
|
|
184
|
+
.subarray(contentStartBytes, contentEndBytes)
|
|
185
|
+
.toString("utf8"),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
export class RunnethCliDaemon {
|
|
189
|
+
#sessions = new Map();
|
|
190
|
+
#server = null;
|
|
191
|
+
#socketPath = null;
|
|
192
|
+
async start(options) {
|
|
193
|
+
if (this.#server !== null) {
|
|
194
|
+
throw new Error("runneth-cli daemon is already started");
|
|
195
|
+
}
|
|
196
|
+
if (process.platform !== "win32") {
|
|
197
|
+
await rm(options.socketPath, { force: true });
|
|
198
|
+
await mkdir(path.dirname(options.socketPath), { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const server = net.createServer((socket) => {
|
|
201
|
+
this.#handleSocket(socket);
|
|
202
|
+
});
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
server.once("error", reject);
|
|
205
|
+
server.listen(options.socketPath, () => {
|
|
206
|
+
server.off("error", reject);
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
this.#server = server;
|
|
211
|
+
this.#socketPath = options.socketPath;
|
|
212
|
+
}
|
|
213
|
+
async stop() {
|
|
214
|
+
for (const session of this.#sessions.values()) {
|
|
215
|
+
this.#closeSession(session);
|
|
216
|
+
}
|
|
217
|
+
this.#sessions.clear();
|
|
218
|
+
const server = this.#server;
|
|
219
|
+
this.#server = null;
|
|
220
|
+
if (server !== null) {
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
server.close((error) => {
|
|
223
|
+
if (error) {
|
|
224
|
+
reject(error);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (this.#socketPath !== null && process.platform !== "win32") {
|
|
232
|
+
await rm(this.#socketPath, { force: true });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async handleRequest(request) {
|
|
236
|
+
switch (request.op) {
|
|
237
|
+
case "open": {
|
|
238
|
+
const session = this.#openSession(request);
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
op: "open",
|
|
242
|
+
session: toPublicSession(session),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
case "send": {
|
|
246
|
+
const session = this.#openSession(request);
|
|
247
|
+
const output = await this.#sendQueued(session, request.command);
|
|
248
|
+
return {
|
|
249
|
+
exitCode: output.exitCode,
|
|
250
|
+
ok: true,
|
|
251
|
+
op: "send",
|
|
252
|
+
session: toPublicSession(session),
|
|
253
|
+
stderr: output.stderr,
|
|
254
|
+
stdout: output.stdout,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case "status": {
|
|
258
|
+
const session = this.#sessions.get(request.name) ?? null;
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
op: "status",
|
|
262
|
+
session: session === null ? null : toPublicSession(session),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case "list": {
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
op: "list",
|
|
269
|
+
sessions: [...this.#sessions.values()].map(toPublicSession),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
case "close": {
|
|
273
|
+
const session = this.#sessions.get(request.name);
|
|
274
|
+
if (session === undefined) {
|
|
275
|
+
throw new Error(`Session is not open: ${request.name}`);
|
|
276
|
+
}
|
|
277
|
+
this.#sessions.delete(request.name);
|
|
278
|
+
this.#closeSession(session);
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
op: "close",
|
|
282
|
+
session: toPublicSession(session),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case "ping": {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
op: "ping",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
case "shutdown": {
|
|
292
|
+
setImmediate(() => {
|
|
293
|
+
void this.stop();
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
op: "shutdown",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
default: {
|
|
301
|
+
const exhaustiveCheck = request;
|
|
302
|
+
return exhaustiveCheck;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
#openSession(input) {
|
|
307
|
+
const existing = this.#sessions.get(input.name);
|
|
308
|
+
if (existing !== undefined && !existing.closed) {
|
|
309
|
+
return existing;
|
|
310
|
+
}
|
|
311
|
+
const shell = input.shell?.trim() || defaultShell();
|
|
312
|
+
const child = spawn(shell, [], {
|
|
313
|
+
cwd: input.cwd,
|
|
314
|
+
env: process.env,
|
|
315
|
+
stdio: "pipe",
|
|
316
|
+
});
|
|
317
|
+
child.stdin.setDefaultEncoding("utf8");
|
|
318
|
+
const session = {
|
|
319
|
+
busy: false,
|
|
320
|
+
child,
|
|
321
|
+
closed: false,
|
|
322
|
+
cwd: input.cwd,
|
|
323
|
+
name: input.name,
|
|
324
|
+
queue: Promise.resolve(),
|
|
325
|
+
sessionId: randomUUID(),
|
|
326
|
+
shell,
|
|
327
|
+
startedAt: nowIso(),
|
|
328
|
+
stderr: Buffer.alloc(0),
|
|
329
|
+
stdout: Buffer.alloc(0),
|
|
330
|
+
waiters: new Set(),
|
|
331
|
+
};
|
|
332
|
+
child.stdout.on("data", (chunk) => {
|
|
333
|
+
session.stdout = appendBuffer(session.stdout, chunk);
|
|
334
|
+
notifyWaiters(session);
|
|
335
|
+
});
|
|
336
|
+
child.stderr.on("data", (chunk) => {
|
|
337
|
+
session.stderr = appendBuffer(session.stderr, chunk);
|
|
338
|
+
notifyWaiters(session);
|
|
339
|
+
});
|
|
340
|
+
child.once("close", () => {
|
|
341
|
+
session.closed = true;
|
|
342
|
+
session.busy = false;
|
|
343
|
+
notifyWaiters(session);
|
|
344
|
+
});
|
|
345
|
+
child.once("error", () => {
|
|
346
|
+
session.closed = true;
|
|
347
|
+
session.busy = false;
|
|
348
|
+
notifyWaiters(session);
|
|
349
|
+
});
|
|
350
|
+
this.#sessions.set(input.name, session);
|
|
351
|
+
return session;
|
|
352
|
+
}
|
|
353
|
+
async #sendQueued(session, command) {
|
|
354
|
+
const runPromise = session.queue.then(async () => {
|
|
355
|
+
session.busy = true;
|
|
356
|
+
try {
|
|
357
|
+
return await this.#send(session, command);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
session.busy = false;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
session.queue = runPromise.then(() => undefined, () => undefined);
|
|
364
|
+
return await runPromise;
|
|
365
|
+
}
|
|
366
|
+
async #send(session, command) {
|
|
367
|
+
if (session.closed || !session.child.stdin.writable) {
|
|
368
|
+
throw new Error(`Session is closed: ${session.name}`);
|
|
369
|
+
}
|
|
370
|
+
const envelope = createCommandEnvelope(command);
|
|
371
|
+
const startStdoutLength = session.stdout.length;
|
|
372
|
+
const startStderrLength = session.stderr.length;
|
|
373
|
+
await new Promise((resolve, reject) => {
|
|
374
|
+
session.child.stdin.write(envelope.input, (error) => {
|
|
375
|
+
if (error) {
|
|
376
|
+
reject(error);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
resolve();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
while (true) {
|
|
383
|
+
const stdoutWindow = session.stdout.subarray(startStdoutLength);
|
|
384
|
+
const extracted = extractCommandResult({
|
|
385
|
+
marker: envelope.marker,
|
|
386
|
+
stdout: stdoutWindow,
|
|
387
|
+
});
|
|
388
|
+
if (extracted !== null) {
|
|
389
|
+
const stderr = session.stderr
|
|
390
|
+
.subarray(startStderrLength)
|
|
391
|
+
.toString("utf8");
|
|
392
|
+
session.stdout = session.stdout.subarray(startStdoutLength + extracted.consumedStdoutBytes);
|
|
393
|
+
session.stderr = Buffer.alloc(0);
|
|
394
|
+
return {
|
|
395
|
+
exitCode: extracted.exitCode,
|
|
396
|
+
stderr,
|
|
397
|
+
stdout: extracted.stdout,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (session.closed) {
|
|
401
|
+
throw new Error(`Session exited before command completed: ${session.name}`);
|
|
402
|
+
}
|
|
403
|
+
await waitForSessionChange(session);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
#closeSession(session) {
|
|
407
|
+
session.closed = true;
|
|
408
|
+
session.busy = false;
|
|
409
|
+
notifyWaiters(session);
|
|
410
|
+
if (!session.child.killed) {
|
|
411
|
+
session.child.kill("SIGTERM");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
#handleSocket(socket) {
|
|
415
|
+
let buffer = "";
|
|
416
|
+
let handled = false;
|
|
417
|
+
socket.setEncoding("utf8");
|
|
418
|
+
socket.on("data", (chunk) => {
|
|
419
|
+
if (handled) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
buffer += chunk;
|
|
423
|
+
if (buffer.length > MAX_REQUEST_BYTES) {
|
|
424
|
+
handled = true;
|
|
425
|
+
socket.end(`${JSON.stringify({ ok: false, error: "Request is too large" })}\n`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
429
|
+
if (newlineIndex === -1) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
handled = true;
|
|
433
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
434
|
+
void this.#handleSocketLine(socket, rawLine);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async #handleSocketLine(socket, rawLine) {
|
|
438
|
+
try {
|
|
439
|
+
const request = parseRunnethCliRequest(JSON.parse(rawLine));
|
|
440
|
+
const response = await this.handleRequest(request);
|
|
441
|
+
if (!socket.destroyed) {
|
|
442
|
+
socket.end(`${JSON.stringify(response)}\n`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (!socket.destroyed) {
|
|
447
|
+
socket.end(`${JSON.stringify({
|
|
448
|
+
error: errorMessage(error),
|
|
449
|
+
ok: false,
|
|
450
|
+
})}\n`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export const sendRunnethCliRequest = async (input) => {
|
|
456
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
457
|
+
return await new Promise((resolve, reject) => {
|
|
458
|
+
const socket = net.createConnection(input.socketPath);
|
|
459
|
+
let settled = false;
|
|
460
|
+
let buffer = "";
|
|
461
|
+
const timeout = setTimeout(() => {
|
|
462
|
+
settleError(new Error(`Timed out waiting for runneth-cli response after ${String(timeoutMs)}ms`));
|
|
463
|
+
}, timeoutMs);
|
|
464
|
+
const cleanup = () => {
|
|
465
|
+
clearTimeout(timeout);
|
|
466
|
+
socket.removeAllListeners();
|
|
467
|
+
socket.destroy();
|
|
468
|
+
};
|
|
469
|
+
const settle = (response) => {
|
|
470
|
+
if (settled) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
settled = true;
|
|
474
|
+
cleanup();
|
|
475
|
+
resolve(response);
|
|
476
|
+
};
|
|
477
|
+
const settleError = (error) => {
|
|
478
|
+
if (settled) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
settled = true;
|
|
482
|
+
cleanup();
|
|
483
|
+
reject(error);
|
|
484
|
+
};
|
|
485
|
+
socket.setEncoding("utf8");
|
|
486
|
+
socket.once("connect", () => {
|
|
487
|
+
socket.write(`${JSON.stringify(input.request)}\n`);
|
|
488
|
+
});
|
|
489
|
+
socket.once("error", settleError);
|
|
490
|
+
socket.on("data", (chunk) => {
|
|
491
|
+
buffer += chunk;
|
|
492
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
493
|
+
if (newlineIndex === -1) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const parsed = JSON.parse(buffer.slice(0, newlineIndex));
|
|
498
|
+
if (!isRecord(parsed) || typeof parsed.ok !== "boolean") {
|
|
499
|
+
throw new Error("Invalid runneth-cli daemon response");
|
|
500
|
+
}
|
|
501
|
+
settle(parsed);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
settleError(error instanceof Error ? error : new Error(String(error)));
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
const canConnect = async (socketPath) => {
|
|
510
|
+
try {
|
|
511
|
+
const response = await sendRunnethCliRequest({
|
|
512
|
+
request: { op: "ping" },
|
|
513
|
+
socketPath,
|
|
514
|
+
timeoutMs: SOCKET_CONNECT_TIMEOUT_MS,
|
|
515
|
+
});
|
|
516
|
+
return response.ok === true;
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
export const startDaemonProcess = async (input) => {
|
|
523
|
+
if (process.platform !== "win32") {
|
|
524
|
+
await rm(input.socketPath, { force: true });
|
|
525
|
+
await mkdir(path.dirname(input.socketPath), { recursive: true });
|
|
526
|
+
}
|
|
527
|
+
const child = spawn(process.execPath, [input.cliPath, "daemon", input.socketPath], {
|
|
528
|
+
detached: true,
|
|
529
|
+
env: process.env,
|
|
530
|
+
stdio: "ignore",
|
|
531
|
+
});
|
|
532
|
+
child.unref();
|
|
533
|
+
};
|
|
534
|
+
export const ensureDaemon = async (input) => {
|
|
535
|
+
if (await canConnect(input.socketPath)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
await startDaemonProcess(input);
|
|
539
|
+
const deadline = Date.now() + DAEMON_START_TIMEOUT_MS;
|
|
540
|
+
while (Date.now() < deadline) {
|
|
541
|
+
if (await canConnect(input.socketPath)) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
await delay(25);
|
|
545
|
+
}
|
|
546
|
+
throw new Error("Timed out waiting for runneth-cli daemon to start");
|
|
547
|
+
};
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type OAuthTokenEndpointAuthMethod = "client_secret_basic" | "client_secret_post" | "none";
|
|
2
|
+
export type OAuthClientRegistration = Readonly<{
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientName: string;
|
|
5
|
+
clientSecret?: string;
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
|
|
8
|
+
}>;
|
|
9
|
+
export type OAuthServerMetadata = Readonly<{
|
|
10
|
+
authorizationEndpoint: string;
|
|
11
|
+
issuer: string;
|
|
12
|
+
registrationEndpoint: string;
|
|
13
|
+
tokenEndpoint: string;
|
|
14
|
+
}>;
|
|
15
|
+
export type OAuthStoredToken = Readonly<{
|
|
16
|
+
accessToken: string;
|
|
17
|
+
expiresAt?: string;
|
|
18
|
+
idToken?: string;
|
|
19
|
+
refreshToken?: string;
|
|
20
|
+
scope?: string;
|
|
21
|
+
tokenType: string;
|
|
22
|
+
}>;
|
|
23
|
+
export type OAuthStoredCredential = Readonly<{
|
|
24
|
+
authorizationServer: OAuthServerMetadata;
|
|
25
|
+
client: OAuthClientRegistration;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
requestedResourceUrl: string;
|
|
28
|
+
resource: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
token: OAuthStoredToken;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
version: 1;
|
|
33
|
+
}>;
|
|
34
|
+
export type OAuthCredentialStatus = Readonly<{
|
|
35
|
+
authenticated: boolean;
|
|
36
|
+
clientId?: string;
|
|
37
|
+
credentialPath: string;
|
|
38
|
+
expiresAt?: string;
|
|
39
|
+
requestedResourceUrl: string;
|
|
40
|
+
resource?: string;
|
|
41
|
+
scope?: string;
|
|
42
|
+
tokenType?: string;
|
|
43
|
+
}>;
|
|
44
|
+
export type OAuthLoginOptions = Readonly<{
|
|
45
|
+
authorizationUrlHandler?: (authorizationUrl: string) => Promise<void> | void;
|
|
46
|
+
clientName?: string;
|
|
47
|
+
homePath?: string;
|
|
48
|
+
openBrowser?: boolean;
|
|
49
|
+
resourceUrl: string;
|
|
50
|
+
scope?: string;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}>;
|
|
53
|
+
export type OAuthTokenResult = Readonly<{
|
|
54
|
+
accessToken: string;
|
|
55
|
+
credential: OAuthStoredCredential;
|
|
56
|
+
credentialPath: string;
|
|
57
|
+
expiresAt?: string;
|
|
58
|
+
tokenType: string;
|
|
59
|
+
}>;
|
|
60
|
+
export declare const resolveOAuthCredentialPath: (input: {
|
|
61
|
+
readonly homePath?: string;
|
|
62
|
+
readonly resourceUrl: string;
|
|
63
|
+
}) => string;
|
|
64
|
+
export declare const readOAuthCredential: (input: {
|
|
65
|
+
readonly homePath?: string;
|
|
66
|
+
readonly resourceUrl: string;
|
|
67
|
+
}) => Promise<OAuthStoredCredential | null>;
|
|
68
|
+
export declare const readOAuthCredentialStatus: (input: {
|
|
69
|
+
readonly homePath?: string;
|
|
70
|
+
readonly resourceUrl: string;
|
|
71
|
+
}) => Promise<OAuthCredentialStatus>;
|
|
72
|
+
export declare const logoutOAuthCredential: (input: {
|
|
73
|
+
readonly homePath?: string;
|
|
74
|
+
readonly resourceUrl: string;
|
|
75
|
+
}) => Promise<OAuthCredentialStatus>;
|
|
76
|
+
export declare const loginWithOAuth: (options: OAuthLoginOptions) => Promise<OAuthTokenResult>;
|
|
77
|
+
export declare const getOAuthAccessToken: (input: {
|
|
78
|
+
readonly homePath?: string;
|
|
79
|
+
readonly resourceUrl: string;
|
|
80
|
+
}) => Promise<OAuthTokenResult>;
|