@runneth/cli 0.0.0-sha.14f7fe073a12.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 +216 -0
- package/dist/build-defaults.d.ts +6 -0
- package/dist/build-defaults.js +36 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1533 -0
- package/dist/index.d.ts +8406 -0
- package/dist/index.js +2 -0
- package/dist/src.js +11321 -0
- package/package.json +42 -0
- package/skills/runneth/SKILL.md +177 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolveBuildDefaultMcpResourceUrl } from "./build-defaults.js";
|
|
3
|
+
import { B as readOAuthCredentialStatus, C as setDefaultRunnethSshTarget, D as runRunnethConversationMachineCommand, I as getOAuthAccessToken, L as loginWithOAuth, M as parseRunnethConversationArgs, N as resolveRunnethConversationOptions, P as runRunnethConversation, R as logoutOAuthCredential, S as saveRunnethSshTarget, T as runRunnethVmCopy, U as resolveSocketPath, _ as resolveRunnethSshTarget, a as normalizeSessionName, b as runOpenSsh, d as installRunnethSshAccess, f as normalizeRunnethSshTargetName, l as installRunnethSkills, m as removeRunnethSshTarget, p as readRunnethSshTargetStore, r as ensureDaemon, s as sendRunnethCliRequest, t as RunnethCliDaemon, w as assertRunnethVmCopyTargetsUseSameResource, x as runRunnethSshProxy } from "./src.js";
|
|
4
|
+
import { readFile, rm } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process$1 from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
10
|
+
//#region src/ssh-stdio.ts
|
|
11
|
+
const ALLOWED_PROCESS_SIGNALS = [
|
|
12
|
+
"SIGHUP",
|
|
13
|
+
"SIGINT",
|
|
14
|
+
"SIGKILL",
|
|
15
|
+
"SIGTERM"
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_EXEC_TIMEOUT_MS = 6e5;
|
|
18
|
+
const DEFAULT_MASTER_READY_TIMEOUT_MS = 1e4;
|
|
19
|
+
const DEFAULT_MAX_FRAME_BYTES = 1048576;
|
|
20
|
+
const DEFAULT_MAX_PROCESSES = 16;
|
|
21
|
+
const PROCESS_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
22
|
+
const nowIso = () => {
|
|
23
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
24
|
+
};
|
|
25
|
+
const isRecord$1 = (value) => {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
};
|
|
28
|
+
const errorMessage = (error) => {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
};
|
|
31
|
+
const parseString = (record, key) => {
|
|
32
|
+
const value = record[key];
|
|
33
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${key} must be a non-empty string`);
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const parseProcessId = (value) => {
|
|
37
|
+
if (!PROCESS_ID_PATTERN.test(value)) throw new Error(`Invalid process id: ${value}. Use letters, numbers, dot, underscore, dash, and colon only.`);
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
const parsePositiveInteger = (record, key) => {
|
|
41
|
+
const value = record[key];
|
|
42
|
+
if (value === void 0) return;
|
|
43
|
+
if (!Number.isInteger(value) || typeof value !== "number" || value <= 0) throw new Error(`${key} must be a positive integer`);
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
const parseParams = (record) => {
|
|
47
|
+
const params = record.params;
|
|
48
|
+
if (!isRecord$1(params)) throw new Error("params must be a JSON object");
|
|
49
|
+
return params;
|
|
50
|
+
};
|
|
51
|
+
const parseProcessSignal = (record) => {
|
|
52
|
+
const value = parseString(record, "signal");
|
|
53
|
+
if (!ALLOWED_PROCESS_SIGNALS.includes(value)) throw new Error(`Unsupported signal: ${value}`);
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
const parseRunnethSshStdioRequest = (value) => {
|
|
57
|
+
if (!isRecord$1(value)) throw new Error("Request must be a JSON object");
|
|
58
|
+
const id = parseProcessId(parseString(value, "id"));
|
|
59
|
+
const method = parseString(value, "method");
|
|
60
|
+
switch (method) {
|
|
61
|
+
case "exec": {
|
|
62
|
+
const params = parseParams(value);
|
|
63
|
+
const timeoutMs = parsePositiveInteger(params, "timeoutMs");
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
method,
|
|
67
|
+
params: {
|
|
68
|
+
command: parseString(params, "command"),
|
|
69
|
+
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "spawn": return {
|
|
74
|
+
id,
|
|
75
|
+
method,
|
|
76
|
+
params: { command: parseString(parseParams(value), "command") }
|
|
77
|
+
};
|
|
78
|
+
case "stdin": {
|
|
79
|
+
const data = parseParams(value).data;
|
|
80
|
+
if (typeof data !== "string") throw new Error("data must be a string");
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
method,
|
|
84
|
+
params: { data }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case "signal": return {
|
|
88
|
+
id,
|
|
89
|
+
method,
|
|
90
|
+
params: { signal: parseProcessSignal(parseParams(value)) }
|
|
91
|
+
};
|
|
92
|
+
case "close":
|
|
93
|
+
case "list":
|
|
94
|
+
case "ping": return {
|
|
95
|
+
id,
|
|
96
|
+
method
|
|
97
|
+
};
|
|
98
|
+
default: throw new Error(`Unknown SSH stdio method: ${method}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const writeChunk = async (stream, chunk) => {
|
|
102
|
+
if (stream.write(chunk)) return;
|
|
103
|
+
await new Promise((resolve) => {
|
|
104
|
+
stream.once("drain", () => {
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
var JsonLineWriter = class {
|
|
110
|
+
#stream;
|
|
111
|
+
#queue = Promise.resolve();
|
|
112
|
+
constructor(stream) {
|
|
113
|
+
this.#stream = stream;
|
|
114
|
+
}
|
|
115
|
+
async flush() {
|
|
116
|
+
await this.#queue;
|
|
117
|
+
}
|
|
118
|
+
write(payload) {
|
|
119
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
120
|
+
const next = this.#queue.then(async () => {
|
|
121
|
+
await writeChunk(this.#stream, line);
|
|
122
|
+
});
|
|
123
|
+
this.#queue = next.then(() => void 0, () => void 0);
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const waitForChildExit = async (child) => {
|
|
128
|
+
return await new Promise((resolve, reject) => {
|
|
129
|
+
child.once("error", reject);
|
|
130
|
+
child.once("close", (code, signal) => {
|
|
131
|
+
resolve({
|
|
132
|
+
code,
|
|
133
|
+
signal
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const runProcessExitCode = async (input) => {
|
|
139
|
+
return await new Promise((resolve, reject) => {
|
|
140
|
+
const child = spawn(input.sshCommand, [...input.args], {
|
|
141
|
+
env: input.env,
|
|
142
|
+
stdio: [
|
|
143
|
+
"ignore",
|
|
144
|
+
"ignore",
|
|
145
|
+
"ignore"
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
child.once("error", reject);
|
|
149
|
+
child.once("close", (code, signal) => {
|
|
150
|
+
if (signal !== null) {
|
|
151
|
+
resolve(1);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
resolve(code ?? 1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
var RunnethSshControlMaster = class {
|
|
159
|
+
#controlPath;
|
|
160
|
+
#env;
|
|
161
|
+
#masterReadyTimeoutMs;
|
|
162
|
+
#setup;
|
|
163
|
+
#sshCommand;
|
|
164
|
+
#master = null;
|
|
165
|
+
#masterClosed = false;
|
|
166
|
+
#masterExit = null;
|
|
167
|
+
#masterStderr = "";
|
|
168
|
+
constructor(options) {
|
|
169
|
+
this.#controlPath = options.controlPath;
|
|
170
|
+
this.#env = options.env;
|
|
171
|
+
this.#masterReadyTimeoutMs = options.masterReadyTimeoutMs;
|
|
172
|
+
this.#setup = options.setup;
|
|
173
|
+
this.#sshCommand = options.sshCommand;
|
|
174
|
+
}
|
|
175
|
+
async start() {
|
|
176
|
+
await rm(this.#controlPath, { force: true });
|
|
177
|
+
const master = spawn(this.#sshCommand, [
|
|
178
|
+
"-F",
|
|
179
|
+
this.#setup.configPath,
|
|
180
|
+
"-S",
|
|
181
|
+
this.#controlPath,
|
|
182
|
+
"-o",
|
|
183
|
+
"ControlMaster=yes",
|
|
184
|
+
"-o",
|
|
185
|
+
"ControlPersist=no",
|
|
186
|
+
"-N",
|
|
187
|
+
this.#setup.hostAlias
|
|
188
|
+
], {
|
|
189
|
+
env: this.#env,
|
|
190
|
+
stdio: [
|
|
191
|
+
"ignore",
|
|
192
|
+
"ignore",
|
|
193
|
+
"pipe"
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
this.#master = master;
|
|
197
|
+
master.stderr?.on("data", (chunk) => {
|
|
198
|
+
this.#masterStderr += String(chunk);
|
|
199
|
+
});
|
|
200
|
+
this.#masterExit = waitForChildExit(master).then((exit) => {
|
|
201
|
+
this.#masterClosed = true;
|
|
202
|
+
return exit;
|
|
203
|
+
});
|
|
204
|
+
const deadline = Date.now() + this.#masterReadyTimeoutMs;
|
|
205
|
+
while (Date.now() < deadline) {
|
|
206
|
+
if (this.#masterClosed) throw new Error(this.#formatMasterExitError());
|
|
207
|
+
if (await this.check() === 0) return;
|
|
208
|
+
await setTimeout$1(50);
|
|
209
|
+
}
|
|
210
|
+
throw new Error(this.#formatMasterExitError("Timed out opening SSH master"));
|
|
211
|
+
}
|
|
212
|
+
async check() {
|
|
213
|
+
return await runProcessExitCode({
|
|
214
|
+
args: [
|
|
215
|
+
"-F",
|
|
216
|
+
this.#setup.configPath,
|
|
217
|
+
"-S",
|
|
218
|
+
this.#controlPath,
|
|
219
|
+
"-O",
|
|
220
|
+
"check",
|
|
221
|
+
this.#setup.hostAlias
|
|
222
|
+
],
|
|
223
|
+
env: this.#env,
|
|
224
|
+
sshCommand: this.#sshCommand
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async requireReady() {
|
|
228
|
+
if (this.#masterClosed || await this.check() !== 0) throw new Error(this.#formatMasterExitError("SSH master is not running"));
|
|
229
|
+
}
|
|
230
|
+
spawnRemoteCommand(command) {
|
|
231
|
+
return spawn(this.#sshCommand, [
|
|
232
|
+
"-F",
|
|
233
|
+
this.#setup.configPath,
|
|
234
|
+
"-S",
|
|
235
|
+
this.#controlPath,
|
|
236
|
+
"-o",
|
|
237
|
+
"ControlMaster=no",
|
|
238
|
+
this.#setup.hostAlias,
|
|
239
|
+
command
|
|
240
|
+
], {
|
|
241
|
+
env: this.#env,
|
|
242
|
+
stdio: "pipe"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async stop() {
|
|
246
|
+
const master = this.#master;
|
|
247
|
+
if (master === null || this.#masterExit === null || this.#masterClosed) {
|
|
248
|
+
await rm(this.#controlPath, { force: true });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
master.kill("SIGTERM");
|
|
252
|
+
const timeout = setTimeout$1(5e3).then(() => {
|
|
253
|
+
if (!this.#masterClosed) master.kill("SIGKILL");
|
|
254
|
+
});
|
|
255
|
+
await Promise.race([this.#masterExit, timeout]);
|
|
256
|
+
await this.#masterExit;
|
|
257
|
+
await rm(this.#controlPath, { force: true });
|
|
258
|
+
}
|
|
259
|
+
#formatMasterExitError(prefix = "SSH master exited before it was ready") {
|
|
260
|
+
const stderr = this.#masterStderr.trim();
|
|
261
|
+
if (stderr.length === 0) return prefix;
|
|
262
|
+
return `${prefix}: ${stderr}`;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
var RunnethSshStdioController = class {
|
|
266
|
+
#defaultTimeoutMs;
|
|
267
|
+
#master;
|
|
268
|
+
#maxFrameBytes;
|
|
269
|
+
#maxProcesses;
|
|
270
|
+
#processes = /* @__PURE__ */ new Map();
|
|
271
|
+
#setup;
|
|
272
|
+
#stdin;
|
|
273
|
+
#stderr;
|
|
274
|
+
#writer;
|
|
275
|
+
#stopping = false;
|
|
276
|
+
constructor(options) {
|
|
277
|
+
const setup = options.setup;
|
|
278
|
+
const env = options.env ?? process.env;
|
|
279
|
+
const controlPath = path.join(path.dirname(setup.configPath), "control.sock");
|
|
280
|
+
this.#defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
281
|
+
this.#maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
282
|
+
this.#maxProcesses = options.maxProcesses ?? DEFAULT_MAX_PROCESSES;
|
|
283
|
+
this.#setup = setup;
|
|
284
|
+
this.#stdin = options.streams?.stdin ?? process.stdin;
|
|
285
|
+
this.#stderr = options.streams?.stderr ?? process.stderr;
|
|
286
|
+
this.#writer = new JsonLineWriter(options.streams?.stdout ?? process.stdout);
|
|
287
|
+
this.#master = new RunnethSshControlMaster({
|
|
288
|
+
controlPath,
|
|
289
|
+
env,
|
|
290
|
+
masterReadyTimeoutMs: options.masterReadyTimeoutMs ?? DEFAULT_MASTER_READY_TIMEOUT_MS,
|
|
291
|
+
setup,
|
|
292
|
+
sshCommand: options.sshCommand ?? "ssh"
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async start() {
|
|
296
|
+
await this.#master.start();
|
|
297
|
+
await writeChunk(this.#stderr, `SSH stdio master ${this.#setup.hostAlias} ready\n`);
|
|
298
|
+
await this.#writer.write({
|
|
299
|
+
hostAlias: this.#setup.hostAlias,
|
|
300
|
+
ok: true,
|
|
301
|
+
protocol: "runneth-ssh-stdio",
|
|
302
|
+
type: "ready",
|
|
303
|
+
version: 1,
|
|
304
|
+
...this.#setup.targetName === void 0 ? {} : { targetName: this.#setup.targetName }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async run() {
|
|
308
|
+
let buffer = "";
|
|
309
|
+
for await (const chunk of this.#stdin) {
|
|
310
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
311
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
312
|
+
while (newlineIndex !== -1) {
|
|
313
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
314
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
315
|
+
if (Buffer.byteLength(rawLine, "utf8") > this.#maxFrameBytes) {
|
|
316
|
+
await this.#writer.write({
|
|
317
|
+
message: `SSH stdio frame exceeds ${String(this.#maxFrameBytes)} bytes`,
|
|
318
|
+
ok: false,
|
|
319
|
+
type: "error"
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (rawLine.trim().length > 0) await this.#handleLine(rawLine);
|
|
324
|
+
newlineIndex = buffer.indexOf("\n");
|
|
325
|
+
}
|
|
326
|
+
if (Buffer.byteLength(buffer, "utf8") > this.#maxFrameBytes) {
|
|
327
|
+
await this.#writer.write({
|
|
328
|
+
message: `SSH stdio frame exceeds ${String(this.#maxFrameBytes)} bytes`,
|
|
329
|
+
ok: false,
|
|
330
|
+
type: "error"
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async stop() {
|
|
337
|
+
this.#stopping = true;
|
|
338
|
+
for (const processInfo of this.#processes.values()) processInfo.child.kill("SIGTERM");
|
|
339
|
+
await this.#master.stop();
|
|
340
|
+
await this.#writer.flush();
|
|
341
|
+
}
|
|
342
|
+
async #handleLine(rawLine) {
|
|
343
|
+
let request;
|
|
344
|
+
try {
|
|
345
|
+
request = parseRunnethSshStdioRequest(JSON.parse(rawLine));
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await this.#writer.write({
|
|
348
|
+
message: errorMessage(error),
|
|
349
|
+
ok: false,
|
|
350
|
+
type: "error"
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
switch (request.method) {
|
|
356
|
+
case "exec":
|
|
357
|
+
this.#exec(request).catch((error) => {
|
|
358
|
+
this.#writeRequestError(request.id, error);
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
case "spawn":
|
|
362
|
+
await this.#spawn(request);
|
|
363
|
+
return;
|
|
364
|
+
case "stdin":
|
|
365
|
+
await this.#writeProcessStdin(request);
|
|
366
|
+
return;
|
|
367
|
+
case "signal":
|
|
368
|
+
await this.#signalProcess(request);
|
|
369
|
+
return;
|
|
370
|
+
case "close":
|
|
371
|
+
await this.#closeProcess(request.id);
|
|
372
|
+
return;
|
|
373
|
+
case "list":
|
|
374
|
+
await this.#writer.write({
|
|
375
|
+
id: request.id,
|
|
376
|
+
ok: true,
|
|
377
|
+
processes: [...this.#processes.values()].map((processInfo) => {
|
|
378
|
+
return {
|
|
379
|
+
command: processInfo.command,
|
|
380
|
+
processId: processInfo.processId,
|
|
381
|
+
startedAt: processInfo.startedAt
|
|
382
|
+
};
|
|
383
|
+
}),
|
|
384
|
+
type: "list"
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
case "ping":
|
|
388
|
+
await this.#writer.write({
|
|
389
|
+
id: request.id,
|
|
390
|
+
ok: true,
|
|
391
|
+
type: "pong"
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
default: return request;
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
await this.#writeRequestError(request.id, error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async #exec(request) {
|
|
401
|
+
await this.#master.requireReady();
|
|
402
|
+
const child = this.#master.spawnRemoteCommand(request.params.command);
|
|
403
|
+
const timeoutMs = request.params.timeoutMs ?? this.#defaultTimeoutMs;
|
|
404
|
+
let timedOut = false;
|
|
405
|
+
const timeout = setTimeout(() => {
|
|
406
|
+
timedOut = true;
|
|
407
|
+
this.#writer.write({
|
|
408
|
+
id: request.id,
|
|
409
|
+
message: `Command timed out after ${String(timeoutMs)}ms`,
|
|
410
|
+
ok: false,
|
|
411
|
+
type: "error"
|
|
412
|
+
});
|
|
413
|
+
child.kill("SIGTERM");
|
|
414
|
+
}, timeoutMs);
|
|
415
|
+
child.stdout.on("data", (chunk) => {
|
|
416
|
+
this.#writer.write({
|
|
417
|
+
data: String(chunk),
|
|
418
|
+
id: request.id,
|
|
419
|
+
type: "stdout"
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
child.stderr.on("data", (chunk) => {
|
|
423
|
+
this.#writer.write({
|
|
424
|
+
data: String(chunk),
|
|
425
|
+
id: request.id,
|
|
426
|
+
type: "stderr"
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
child.once("error", (error) => {
|
|
430
|
+
clearTimeout(timeout);
|
|
431
|
+
this.#writeRequestError(request.id, error);
|
|
432
|
+
});
|
|
433
|
+
child.once("close", (code, signal) => {
|
|
434
|
+
clearTimeout(timeout);
|
|
435
|
+
this.#writer.write({
|
|
436
|
+
code,
|
|
437
|
+
id: request.id,
|
|
438
|
+
ok: !timedOut,
|
|
439
|
+
signal,
|
|
440
|
+
type: "exit"
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
async #spawn(request) {
|
|
445
|
+
if (this.#processes.has(request.id)) throw new Error(`Process is already running: ${request.id}`);
|
|
446
|
+
if (this.#processes.size >= this.#maxProcesses) throw new Error(`SSH stdio process limit reached: ${String(this.#maxProcesses)}`);
|
|
447
|
+
await this.#master.requireReady();
|
|
448
|
+
const child = this.#master.spawnRemoteCommand(request.params.command);
|
|
449
|
+
const processInfo = {
|
|
450
|
+
child,
|
|
451
|
+
command: request.params.command,
|
|
452
|
+
processId: request.id,
|
|
453
|
+
startedAt: nowIso()
|
|
454
|
+
};
|
|
455
|
+
this.#processes.set(request.id, processInfo);
|
|
456
|
+
child.once("spawn", () => {
|
|
457
|
+
this.#writer.write({
|
|
458
|
+
command: request.params.command,
|
|
459
|
+
id: request.id,
|
|
460
|
+
ok: true,
|
|
461
|
+
processId: request.id,
|
|
462
|
+
type: "started"
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
child.stdout.on("data", (chunk) => {
|
|
466
|
+
this.#writer.write({
|
|
467
|
+
data: String(chunk),
|
|
468
|
+
id: request.id,
|
|
469
|
+
processId: request.id,
|
|
470
|
+
type: "stdout"
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
child.stderr.on("data", (chunk) => {
|
|
474
|
+
this.#writer.write({
|
|
475
|
+
data: String(chunk),
|
|
476
|
+
id: request.id,
|
|
477
|
+
processId: request.id,
|
|
478
|
+
type: "stderr"
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
child.once("error", (error) => {
|
|
482
|
+
this.#processes.delete(request.id);
|
|
483
|
+
this.#writeRequestError(request.id, error);
|
|
484
|
+
});
|
|
485
|
+
child.once("close", (code, signal) => {
|
|
486
|
+
this.#processes.delete(request.id);
|
|
487
|
+
if (this.#stopping) return;
|
|
488
|
+
this.#writer.write({
|
|
489
|
+
code,
|
|
490
|
+
id: request.id,
|
|
491
|
+
ok: true,
|
|
492
|
+
processId: request.id,
|
|
493
|
+
signal,
|
|
494
|
+
type: "exit"
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async #writeProcessStdin(request) {
|
|
499
|
+
await writeChunk(this.#requireProcess(request.id).child.stdin, request.params.data);
|
|
500
|
+
await this.#writer.write({
|
|
501
|
+
id: request.id,
|
|
502
|
+
ok: true,
|
|
503
|
+
type: "ack"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async #signalProcess(request) {
|
|
507
|
+
if (!this.#requireProcess(request.id).child.kill(request.params.signal)) throw new Error(`Unable to signal process: ${request.id}`);
|
|
508
|
+
await this.#writer.write({
|
|
509
|
+
id: request.id,
|
|
510
|
+
ok: true,
|
|
511
|
+
signal: request.params.signal,
|
|
512
|
+
type: "ack"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async #closeProcess(processId) {
|
|
516
|
+
if (!this.#requireProcess(processId).child.kill("SIGTERM")) throw new Error(`Unable to close process: ${processId}`);
|
|
517
|
+
await this.#writer.write({
|
|
518
|
+
id: processId,
|
|
519
|
+
ok: true,
|
|
520
|
+
type: "ack"
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
#requireProcess(processId) {
|
|
524
|
+
const processInfo = this.#processes.get(processId);
|
|
525
|
+
if (processInfo === void 0) throw new Error(`Process is not running: ${processId}`);
|
|
526
|
+
return processInfo;
|
|
527
|
+
}
|
|
528
|
+
async #writeRequestError(id, error) {
|
|
529
|
+
await this.#writer.write({
|
|
530
|
+
id,
|
|
531
|
+
message: errorMessage(error),
|
|
532
|
+
ok: false,
|
|
533
|
+
type: "error"
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const runRunnethSshStdio = async (options) => {
|
|
538
|
+
const controller = new RunnethSshStdioController(options);
|
|
539
|
+
await controller.start();
|
|
540
|
+
try {
|
|
541
|
+
await controller.run();
|
|
542
|
+
} finally {
|
|
543
|
+
await controller.stop();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/cli.ts
|
|
548
|
+
const DEFAULT_TIMEOUT_MS = 6e5;
|
|
549
|
+
const DEFAULT_OAUTH_CLIENT_NAME = "Runneth MCP";
|
|
550
|
+
const DEFAULT_OAUTH_SCOPE = "openid profile email offline_access";
|
|
551
|
+
const MAX_STDIN_BYTES = 1048576;
|
|
552
|
+
const isRecord = (value) => {
|
|
553
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
554
|
+
};
|
|
555
|
+
const parseStringField = (record, key) => {
|
|
556
|
+
const value = record[key];
|
|
557
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string`);
|
|
558
|
+
return value;
|
|
559
|
+
};
|
|
560
|
+
const parseOptionalStringField = (record, key) => {
|
|
561
|
+
const value = record[key];
|
|
562
|
+
if (value === void 0) return;
|
|
563
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string`);
|
|
564
|
+
return value;
|
|
565
|
+
};
|
|
566
|
+
const resolveDefaultResourceUrl = () => {
|
|
567
|
+
return resolveBuildDefaultMcpResourceUrl();
|
|
568
|
+
};
|
|
569
|
+
const requireResourceUrl = (resourceUrl, commandDescription) => {
|
|
570
|
+
if (resourceUrl === void 0) throw new Error(`${commandDescription} requires --resource <url> because this CLI build does not include MONDRIAN_API_URL`);
|
|
571
|
+
return resourceUrl;
|
|
572
|
+
};
|
|
573
|
+
const readStdin = async () => {
|
|
574
|
+
const chunks = [];
|
|
575
|
+
let totalBytes = 0;
|
|
576
|
+
for await (const chunk of process$1.stdin) {
|
|
577
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
578
|
+
totalBytes += buffer.length;
|
|
579
|
+
if (totalBytes > MAX_STDIN_BYTES) throw new Error(`stdin input exceeds ${String(MAX_STDIN_BYTES)} bytes`);
|
|
580
|
+
chunks.push(buffer);
|
|
581
|
+
}
|
|
582
|
+
return Buffer.concat(chunks, totalBytes).toString("utf8");
|
|
583
|
+
};
|
|
584
|
+
const printHelp = () => {
|
|
585
|
+
process$1.stdout.write([
|
|
586
|
+
"Usage: runneth-cli <command> [options]",
|
|
587
|
+
"",
|
|
588
|
+
"Commands:",
|
|
589
|
+
" open Open or reuse a persistent shell session",
|
|
590
|
+
" send Send a shell command to a persistent session",
|
|
591
|
+
" status Show one session",
|
|
592
|
+
" list List sessions",
|
|
593
|
+
" close Close one session",
|
|
594
|
+
" oauth Authorize against an OAuth-protected MCP resource",
|
|
595
|
+
" chat Start an interactive Runneth terminal conversation",
|
|
596
|
+
" conversation",
|
|
597
|
+
" Create, send, inspect, or run a JSONL conversation session",
|
|
598
|
+
" ssh Connect to Runneth runtime SSH through OAuth",
|
|
599
|
+
" copy Copy one VM path to another VM through Builder file-share upload",
|
|
600
|
+
" skills Install or update bundled Claude/Codex skills",
|
|
601
|
+
" shutdown Stop the local runneth-cli daemon",
|
|
602
|
+
"",
|
|
603
|
+
"Examples:",
|
|
604
|
+
" runneth-cli open",
|
|
605
|
+
" runneth-cli send 'cd ~/project && pnpm test'",
|
|
606
|
+
" runneth-cli send -- git status --short",
|
|
607
|
+
" printf 'cd ~/project\\npwd\\n' | runneth-cli send --stdin",
|
|
608
|
+
" runneth-cli oauth login",
|
|
609
|
+
" runneth-cli chat --workspace <workspace-id>",
|
|
610
|
+
" runneth-cli conversation create --workspace <workspace-id> --json",
|
|
611
|
+
" runneth-cli ssh target add primary-vm --host 93c7ca56-debe-4b95-8be2-a873afe72234.app.runneth.com --default",
|
|
612
|
+
" runneth-cli ssh -- 'pwd'",
|
|
613
|
+
" runneth-cli copy --from source-vm --to destination-vm /agent/project /agent/imports/project",
|
|
614
|
+
" runneth-cli skills install --agent all",
|
|
615
|
+
""
|
|
616
|
+
].join("\n"));
|
|
617
|
+
};
|
|
618
|
+
const parseNumber = (rawValue, label) => {
|
|
619
|
+
const value = Number(rawValue);
|
|
620
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
|
|
621
|
+
return value;
|
|
622
|
+
};
|
|
623
|
+
const parseArgs = (argv) => {
|
|
624
|
+
const values = [];
|
|
625
|
+
let cwd;
|
|
626
|
+
let name = "default";
|
|
627
|
+
let shell;
|
|
628
|
+
let stdin = false;
|
|
629
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
630
|
+
let parseOptions = true;
|
|
631
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
632
|
+
const token = argv[index];
|
|
633
|
+
if (parseOptions && token === "--") {
|
|
634
|
+
parseOptions = false;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (!parseOptions || !token.startsWith("--")) {
|
|
638
|
+
values.push(token);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const readValue = (label) => {
|
|
642
|
+
const value = argv[index + 1];
|
|
643
|
+
if (value === void 0) throw new Error(`Missing value for ${label}`);
|
|
644
|
+
index += 1;
|
|
645
|
+
return value;
|
|
646
|
+
};
|
|
647
|
+
switch (token) {
|
|
648
|
+
case "--cwd":
|
|
649
|
+
cwd = readValue(token);
|
|
650
|
+
break;
|
|
651
|
+
case "--name":
|
|
652
|
+
name = normalizeSessionName(readValue(token));
|
|
653
|
+
break;
|
|
654
|
+
case "--shell":
|
|
655
|
+
shell = readValue(token);
|
|
656
|
+
break;
|
|
657
|
+
case "--stdin":
|
|
658
|
+
stdin = true;
|
|
659
|
+
break;
|
|
660
|
+
case "--timeout-ms":
|
|
661
|
+
timeoutMs = parseNumber(readValue(token), token);
|
|
662
|
+
break;
|
|
663
|
+
default: throw new Error(`Unknown option: ${token}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
cwd,
|
|
668
|
+
name,
|
|
669
|
+
shell,
|
|
670
|
+
stdin,
|
|
671
|
+
timeoutMs,
|
|
672
|
+
values
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
const parseSkillAgent = (value) => {
|
|
676
|
+
if (value === "all") return ["claude", "codex"];
|
|
677
|
+
if (value === "claude" || value === "codex") return [value];
|
|
678
|
+
throw new Error(`Invalid skill agent: ${value}. Use claude, codex, or all.`);
|
|
679
|
+
};
|
|
680
|
+
const parseSkillsArgs = (argv) => {
|
|
681
|
+
let agents = ["claude", "codex"];
|
|
682
|
+
const values = [];
|
|
683
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
684
|
+
const token = argv[index];
|
|
685
|
+
if (token === "--agent") {
|
|
686
|
+
const value = argv[index + 1];
|
|
687
|
+
if (value === void 0) throw new Error("Missing value for --agent");
|
|
688
|
+
agents = parseSkillAgent(value);
|
|
689
|
+
index += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
values.push(token);
|
|
693
|
+
}
|
|
694
|
+
if (values.length > 0) throw new Error("runneth-cli skills install only accepts --agent");
|
|
695
|
+
return { agents };
|
|
696
|
+
};
|
|
697
|
+
const parseOAuthArgs = (argv) => {
|
|
698
|
+
const values = [];
|
|
699
|
+
let clientName = DEFAULT_OAUTH_CLIENT_NAME;
|
|
700
|
+
let openBrowser = true;
|
|
701
|
+
let resourceUrl;
|
|
702
|
+
let scope = DEFAULT_OAUTH_SCOPE;
|
|
703
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
704
|
+
let parseOptions = true;
|
|
705
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
706
|
+
const token = argv[index];
|
|
707
|
+
if (parseOptions && token === "--") {
|
|
708
|
+
parseOptions = false;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (!parseOptions || !token.startsWith("--")) {
|
|
712
|
+
values.push(token);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const readValue = (label) => {
|
|
716
|
+
const value = argv[index + 1];
|
|
717
|
+
if (value === void 0) throw new Error(`Missing value for ${label}`);
|
|
718
|
+
index += 1;
|
|
719
|
+
return value;
|
|
720
|
+
};
|
|
721
|
+
switch (token) {
|
|
722
|
+
case "--client-name":
|
|
723
|
+
clientName = readValue(token);
|
|
724
|
+
break;
|
|
725
|
+
case "--no-open":
|
|
726
|
+
openBrowser = false;
|
|
727
|
+
break;
|
|
728
|
+
case "--resource":
|
|
729
|
+
resourceUrl = readValue(token);
|
|
730
|
+
break;
|
|
731
|
+
case "--scope":
|
|
732
|
+
scope = readValue(token);
|
|
733
|
+
break;
|
|
734
|
+
case "--timeout-ms":
|
|
735
|
+
timeoutMs = parseNumber(readValue(token), token);
|
|
736
|
+
break;
|
|
737
|
+
default: throw new Error(`Unknown option: ${token}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
clientName,
|
|
742
|
+
openBrowser,
|
|
743
|
+
resourceUrl,
|
|
744
|
+
scope,
|
|
745
|
+
timeoutMs,
|
|
746
|
+
values
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
const parseSshArgs = (argv) => {
|
|
750
|
+
const values = [];
|
|
751
|
+
let clientName = DEFAULT_OAUTH_CLIENT_NAME;
|
|
752
|
+
let identityFilePath;
|
|
753
|
+
let makeDefault = false;
|
|
754
|
+
let openBrowser = true;
|
|
755
|
+
let resourceUrl;
|
|
756
|
+
let scope = DEFAULT_OAUTH_SCOPE;
|
|
757
|
+
let sshHost;
|
|
758
|
+
let sshUrl;
|
|
759
|
+
let targetName;
|
|
760
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
761
|
+
let uniqueKey = false;
|
|
762
|
+
let argumentSeparatorSeen = false;
|
|
763
|
+
let parseOptions = true;
|
|
764
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
765
|
+
const token = argv[index];
|
|
766
|
+
if (parseOptions && token === "--") {
|
|
767
|
+
argumentSeparatorSeen = true;
|
|
768
|
+
parseOptions = false;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (!parseOptions || !token.startsWith("--")) {
|
|
772
|
+
values.push(token);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const readValue = (label) => {
|
|
776
|
+
const value = argv[index + 1];
|
|
777
|
+
if (value === void 0) throw new Error(`Missing value for ${label}`);
|
|
778
|
+
index += 1;
|
|
779
|
+
return value;
|
|
780
|
+
};
|
|
781
|
+
switch (token) {
|
|
782
|
+
case "--client-name":
|
|
783
|
+
clientName = readValue(token);
|
|
784
|
+
break;
|
|
785
|
+
case "--default":
|
|
786
|
+
makeDefault = true;
|
|
787
|
+
break;
|
|
788
|
+
case "--host":
|
|
789
|
+
sshHost = readValue(token);
|
|
790
|
+
break;
|
|
791
|
+
case "--identity-file":
|
|
792
|
+
identityFilePath = readValue(token);
|
|
793
|
+
break;
|
|
794
|
+
case "--no-open":
|
|
795
|
+
openBrowser = false;
|
|
796
|
+
break;
|
|
797
|
+
case "--resource":
|
|
798
|
+
resourceUrl = readValue(token);
|
|
799
|
+
break;
|
|
800
|
+
case "--scope":
|
|
801
|
+
scope = readValue(token);
|
|
802
|
+
break;
|
|
803
|
+
case "--ssh-url":
|
|
804
|
+
sshUrl = readValue(token);
|
|
805
|
+
break;
|
|
806
|
+
case "--target":
|
|
807
|
+
targetName = readValue(token);
|
|
808
|
+
break;
|
|
809
|
+
case "--timeout-ms":
|
|
810
|
+
timeoutMs = parseNumber(readValue(token), token);
|
|
811
|
+
break;
|
|
812
|
+
case "--unique-key":
|
|
813
|
+
uniqueKey = true;
|
|
814
|
+
break;
|
|
815
|
+
default: throw new Error(`Unknown option: ${token}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
argumentSeparatorSeen,
|
|
820
|
+
clientName,
|
|
821
|
+
...identityFilePath === void 0 ? {} : { identityFilePath },
|
|
822
|
+
makeDefault,
|
|
823
|
+
openBrowser,
|
|
824
|
+
resourceUrl,
|
|
825
|
+
scope,
|
|
826
|
+
...sshHost === void 0 ? {} : { sshHost },
|
|
827
|
+
sshUrl,
|
|
828
|
+
...targetName === void 0 ? {} : { targetName },
|
|
829
|
+
timeoutMs,
|
|
830
|
+
uniqueKey,
|
|
831
|
+
values
|
|
832
|
+
};
|
|
833
|
+
};
|
|
834
|
+
const parseCopyArgs = (argv) => {
|
|
835
|
+
const values = [];
|
|
836
|
+
let clientName = DEFAULT_OAUTH_CLIENT_NAME;
|
|
837
|
+
let destinationTargetName;
|
|
838
|
+
const ignoredPaths = [];
|
|
839
|
+
let openBrowser = true;
|
|
840
|
+
let scope = DEFAULT_OAUTH_SCOPE;
|
|
841
|
+
let sourceTargetName;
|
|
842
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
843
|
+
let parseOptions = true;
|
|
844
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
845
|
+
const token = argv[index];
|
|
846
|
+
if (parseOptions && token === "--") {
|
|
847
|
+
parseOptions = false;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (!parseOptions || !token.startsWith("--")) {
|
|
851
|
+
values.push(token);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const readValue = (label) => {
|
|
855
|
+
const value = argv[index + 1];
|
|
856
|
+
if (value === void 0) throw new Error(`Missing value for ${label}`);
|
|
857
|
+
index += 1;
|
|
858
|
+
return value;
|
|
859
|
+
};
|
|
860
|
+
switch (token) {
|
|
861
|
+
case "--client-name":
|
|
862
|
+
clientName = readValue(token);
|
|
863
|
+
break;
|
|
864
|
+
case "--from":
|
|
865
|
+
sourceTargetName = normalizeRunnethSshTargetName(readValue(token));
|
|
866
|
+
break;
|
|
867
|
+
case "--ignore":
|
|
868
|
+
ignoredPaths.push(readValue(token));
|
|
869
|
+
break;
|
|
870
|
+
case "--no-open":
|
|
871
|
+
openBrowser = false;
|
|
872
|
+
break;
|
|
873
|
+
case "--scope":
|
|
874
|
+
scope = readValue(token);
|
|
875
|
+
break;
|
|
876
|
+
case "--timeout-ms":
|
|
877
|
+
timeoutMs = parseNumber(readValue(token), token);
|
|
878
|
+
break;
|
|
879
|
+
case "--to":
|
|
880
|
+
destinationTargetName = normalizeRunnethSshTargetName(readValue(token));
|
|
881
|
+
break;
|
|
882
|
+
default: throw new Error(`Unknown option: ${token}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (sourceTargetName === void 0) throw new Error("runneth-cli copy requires --from <target>");
|
|
886
|
+
if (destinationTargetName === void 0) throw new Error("runneth-cli copy requires --to <target>");
|
|
887
|
+
return {
|
|
888
|
+
clientName,
|
|
889
|
+
destinationTargetName,
|
|
890
|
+
ignoredPaths,
|
|
891
|
+
openBrowser,
|
|
892
|
+
scope,
|
|
893
|
+
sourceTargetName,
|
|
894
|
+
timeoutMs,
|
|
895
|
+
values
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
const resolveOAuthResourceUrl = (parsed) => {
|
|
899
|
+
return requireResourceUrl(parsed.resourceUrl ?? parsed.values[0] ?? resolveDefaultResourceUrl(), "runneth-cli oauth");
|
|
900
|
+
};
|
|
901
|
+
const normalizeSshHost = (value) => {
|
|
902
|
+
const sshHost = value.trim();
|
|
903
|
+
if (sshHost.length === 0 || sshHost.includes("/") || sshHost.includes("?") || sshHost.includes("#") || sshHost.includes("@") || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(sshHost) || /\s/u.test(sshHost)) throw new Error("SSH host must be a full hostname without scheme or path");
|
|
904
|
+
const url = new URL(`https://${sshHost}/runneth/ssh`);
|
|
905
|
+
if (url.hostname.length === 0) throw new Error("SSH host must be a full hostname without scheme or path");
|
|
906
|
+
return url.host;
|
|
907
|
+
};
|
|
908
|
+
const resolveSshUrlFromHost = (host) => {
|
|
909
|
+
return `https://${normalizeSshHost(host)}/runneth/ssh`;
|
|
910
|
+
};
|
|
911
|
+
const resolveSshUrlOption = (parsed) => {
|
|
912
|
+
if (parsed.sshUrl !== void 0 && parsed.sshHost !== void 0) throw new Error("Use either --ssh-url or --host, not both");
|
|
913
|
+
if (parsed.sshHost === void 0) return parsed.sshUrl;
|
|
914
|
+
return resolveSshUrlFromHost(parsed.sshHost);
|
|
915
|
+
};
|
|
916
|
+
const resolveSshResourceUrlOption = (parsed) => {
|
|
917
|
+
if (parsed.resourceUrl !== void 0) return parsed.resourceUrl;
|
|
918
|
+
if (parsed.sshHost !== void 0 || parsed.sshUrl !== void 0) return resolveDefaultResourceUrl();
|
|
919
|
+
};
|
|
920
|
+
const resolveSshTargetForCommand = async (parsed) => {
|
|
921
|
+
const sshUrl = resolveSshUrlOption(parsed);
|
|
922
|
+
const resourceUrl = resolveSshResourceUrlOption(parsed);
|
|
923
|
+
if (sshUrl !== void 0 && resourceUrl === void 0) requireResourceUrl(resourceUrl, "runneth-cli ssh");
|
|
924
|
+
return await resolveRunnethSshTarget({
|
|
925
|
+
resourceUrl,
|
|
926
|
+
sshUrl,
|
|
927
|
+
targetName: parsed.targetName
|
|
928
|
+
});
|
|
929
|
+
};
|
|
930
|
+
const assertNoSshTargetDefaultFlag = (parsed) => {
|
|
931
|
+
if (parsed.makeDefault) throw new Error("--default is only valid for runneth-cli ssh target add");
|
|
932
|
+
};
|
|
933
|
+
const assertSshRemoteCommandSeparator = (parsed) => {
|
|
934
|
+
if (parsed.values.length > 0 && !parsed.argumentSeparatorSeen) throw new Error("Remote SSH commands must follow --");
|
|
935
|
+
};
|
|
936
|
+
const assertNoSshKeyOptions = (parsed, commandDescription) => {
|
|
937
|
+
if (parsed.identityFilePath !== void 0 || parsed.uniqueKey) throw new Error(`${commandDescription} does not accept SSH key options`);
|
|
938
|
+
};
|
|
939
|
+
const resolveSshKeyInstallOptions = (parsed) => {
|
|
940
|
+
if (parsed.identityFilePath !== void 0 && parsed.uniqueKey) throw new Error("Use either --identity-file or --unique-key, not both");
|
|
941
|
+
if (parsed.identityFilePath !== void 0) return { identityFilePath: parsed.identityFilePath };
|
|
942
|
+
if (parsed.uniqueKey) return { keyMode: "unique" };
|
|
943
|
+
return {};
|
|
944
|
+
};
|
|
945
|
+
const parseSshTargetImportEntry = (payload, index) => {
|
|
946
|
+
if (!isRecord(payload)) throw new Error(`targets[${String(index)}] must be a JSON object`);
|
|
947
|
+
const name = normalizeRunnethSshTargetName(parseStringField(payload, "name"));
|
|
948
|
+
const host = parseOptionalStringField(payload, "host");
|
|
949
|
+
const resourceUrl = parseOptionalStringField(payload, "resourceUrl");
|
|
950
|
+
const sshUrl = parseOptionalStringField(payload, "sshUrl");
|
|
951
|
+
if (host === void 0 === (sshUrl === void 0)) throw new Error(`targets[${String(index)}] requires exactly one of host or sshUrl`);
|
|
952
|
+
return {
|
|
953
|
+
...host === void 0 ? {} : { host },
|
|
954
|
+
name,
|
|
955
|
+
...resourceUrl === void 0 ? {} : { resourceUrl },
|
|
956
|
+
...sshUrl === void 0 ? {} : { sshUrl }
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
const parseSshTargetImportFile = (payload) => {
|
|
960
|
+
if (!isRecord(payload)) throw new Error("SSH target import file must be a JSON object");
|
|
961
|
+
const rawTargets = payload.targets;
|
|
962
|
+
if (!Array.isArray(rawTargets) || rawTargets.length === 0) throw new Error("SSH target import file requires a non-empty targets array");
|
|
963
|
+
const defaultTargetValue = parseOptionalStringField(payload, "defaultTarget");
|
|
964
|
+
const resourceUrl = parseOptionalStringField(payload, "resourceUrl");
|
|
965
|
+
return {
|
|
966
|
+
...defaultTargetValue === void 0 ? {} : { defaultTarget: normalizeRunnethSshTargetName(defaultTargetValue) },
|
|
967
|
+
...resourceUrl === void 0 ? {} : { resourceUrl },
|
|
968
|
+
targets: rawTargets.map((entry, index) => parseSshTargetImportEntry(entry, index))
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
const readSshTargetImportFile = async (filePath) => {
|
|
972
|
+
const resolvedPath = path.resolve(filePath);
|
|
973
|
+
return parseSshTargetImportFile(JSON.parse(await readFile(resolvedPath, "utf8")));
|
|
974
|
+
};
|
|
975
|
+
const resolveImportEntryResourceUrl = (input) => {
|
|
976
|
+
return requireResourceUrl(input.entry.resourceUrl ?? input.fileResourceUrl ?? input.commandResourceUrl ?? resolveDefaultResourceUrl(), "runneth-cli ssh target import");
|
|
977
|
+
};
|
|
978
|
+
const resolveImportEntrySshUrl = (entry) => {
|
|
979
|
+
if (entry.sshUrl !== void 0) return entry.sshUrl;
|
|
980
|
+
if (entry.host === void 0) throw new Error(`Imported SSH target is missing host: ${entry.name}`);
|
|
981
|
+
return resolveSshUrlFromHost(entry.host);
|
|
982
|
+
};
|
|
983
|
+
const resolveSshTargetImportEntries = (input) => {
|
|
984
|
+
const names = /* @__PURE__ */ new Set();
|
|
985
|
+
for (const target of input.file.targets) {
|
|
986
|
+
if (names.has(target.name)) throw new Error(`Duplicate SSH target in import file: ${target.name}`);
|
|
987
|
+
names.add(target.name);
|
|
988
|
+
}
|
|
989
|
+
if (input.file.defaultTarget !== void 0 && !names.has(input.file.defaultTarget)) throw new Error(`Import defaultTarget does not match an imported target: ${input.file.defaultTarget}`);
|
|
990
|
+
return input.file.targets.map((entry, index) => {
|
|
991
|
+
return {
|
|
992
|
+
makeDefault: entry.name === input.file.defaultTarget || input.file.defaultTarget === void 0 && input.makeFirstDefault && index === 0,
|
|
993
|
+
name: entry.name,
|
|
994
|
+
resourceUrl: resolveImportEntryResourceUrl({
|
|
995
|
+
entry,
|
|
996
|
+
...input.commandResourceUrl === void 0 ? {} : { commandResourceUrl: input.commandResourceUrl },
|
|
997
|
+
...input.file.resourceUrl === void 0 ? {} : { fileResourceUrl: input.file.resourceUrl }
|
|
998
|
+
}),
|
|
999
|
+
sshUrl: resolveImportEntrySshUrl(entry)
|
|
1000
|
+
};
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
const resolveSshOAuthToken = async (parsed, resourceUrl) => {
|
|
1004
|
+
if ((await readOAuthCredentialStatus({ resourceUrl })).authenticated) return await getOAuthAccessToken({ resourceUrl });
|
|
1005
|
+
return await loginWithOAuth({
|
|
1006
|
+
authorizationUrlHandler: (authorizationUrl) => {
|
|
1007
|
+
process$1.stderr.write(`Authorize runneth-cli here:\n${authorizationUrl}\n`);
|
|
1008
|
+
},
|
|
1009
|
+
clientName: parsed.clientName,
|
|
1010
|
+
openBrowser: parsed.openBrowser,
|
|
1011
|
+
resourceUrl,
|
|
1012
|
+
scope: parsed.scope,
|
|
1013
|
+
timeoutMs: parsed.timeoutMs
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
1016
|
+
const writeJson = (response) => {
|
|
1017
|
+
process$1.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
1018
|
+
};
|
|
1019
|
+
const runDaemon = async (argv) => {
|
|
1020
|
+
const socketPath = parseArgs(argv).values[0] ?? resolveSocketPath();
|
|
1021
|
+
await new RunnethCliDaemon().start({ socketPath });
|
|
1022
|
+
};
|
|
1023
|
+
const request = async (payload, timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
1024
|
+
const socketPath = resolveSocketPath();
|
|
1025
|
+
await ensureDaemon({
|
|
1026
|
+
cliPath: fileURLToPath(import.meta.url),
|
|
1027
|
+
socketPath
|
|
1028
|
+
});
|
|
1029
|
+
return await sendRunnethCliRequest({
|
|
1030
|
+
request: payload,
|
|
1031
|
+
socketPath,
|
|
1032
|
+
timeoutMs
|
|
1033
|
+
});
|
|
1034
|
+
};
|
|
1035
|
+
const run = async (argv) => {
|
|
1036
|
+
const command = argv[0];
|
|
1037
|
+
if (command === void 0 || command === "--help" || command === "-h") {
|
|
1038
|
+
printHelp();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (command === "daemon") {
|
|
1042
|
+
await runDaemon(argv.slice(1));
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (command === "open") {
|
|
1046
|
+
const parsed = parseArgs(argv.slice(1));
|
|
1047
|
+
writeJson(await request({
|
|
1048
|
+
cwd: parsed.cwd ?? process$1.cwd(),
|
|
1049
|
+
name: parsed.name,
|
|
1050
|
+
op: "open",
|
|
1051
|
+
...parsed.shell === void 0 ? {} : { shell: parsed.shell }
|
|
1052
|
+
}));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (command === "send") {
|
|
1056
|
+
const parsed = parseArgs(argv.slice(1));
|
|
1057
|
+
const shellCommand = parsed.stdin ? await readStdin() : parsed.values.join(" ");
|
|
1058
|
+
if (shellCommand.trim().length === 0) throw new Error("runneth-cli send requires a command or --stdin");
|
|
1059
|
+
const response = await request({
|
|
1060
|
+
command: shellCommand,
|
|
1061
|
+
cwd: parsed.cwd ?? process$1.cwd(),
|
|
1062
|
+
name: parsed.name,
|
|
1063
|
+
op: "send",
|
|
1064
|
+
...parsed.shell === void 0 ? {} : { shell: parsed.shell }
|
|
1065
|
+
}, parsed.timeoutMs);
|
|
1066
|
+
writeJson(response);
|
|
1067
|
+
if (response.ok && response.op === "send" && response.exitCode !== 0) process$1.exitCode = response.exitCode;
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (command === "status") {
|
|
1071
|
+
writeJson(await request({
|
|
1072
|
+
name: parseArgs(argv.slice(1)).name,
|
|
1073
|
+
op: "status"
|
|
1074
|
+
}));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (command === "list") {
|
|
1078
|
+
writeJson(await request({ op: "list" }));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (command === "chat") {
|
|
1082
|
+
if (argv[1] === "--help" || argv[1] === "-h") {
|
|
1083
|
+
process$1.stdout.write([
|
|
1084
|
+
"Usage: runneth-cli chat --workspace <workspace-id> [options]",
|
|
1085
|
+
"",
|
|
1086
|
+
"Starts an interactive terminal chat against the canonical Runneth conversation API.",
|
|
1087
|
+
"",
|
|
1088
|
+
"Options:",
|
|
1089
|
+
" --api <url> Builder API origin override; official builds include the environment default",
|
|
1090
|
+
" --resource <url> OAuth protected resource URL; defaults to Builder /mcp in published builds",
|
|
1091
|
+
" --workspace <id> Mondrian/Builder workspace id; may also use RUNNETH_WORKSPACE_ID",
|
|
1092
|
+
" --token <token> Bearer token override; omitted uses stored OAuth or starts login",
|
|
1093
|
+
" --auth <mode> mondrian or builder. Defaults to mondrian",
|
|
1094
|
+
" --client-name <name> OAuth dynamic client name. Defaults to Runneth MCP",
|
|
1095
|
+
" --conversation <id> Resume an existing conversation",
|
|
1096
|
+
" --no-open Print the OAuth URL instead of opening a browser",
|
|
1097
|
+
" --scope <scope> OAuth scope. Defaults to openid profile email offline_access",
|
|
1098
|
+
" --tenanth <id> Tenant route id. Defaults to motion",
|
|
1099
|
+
" --title <title> Title for newly-created conversations",
|
|
1100
|
+
" --poll-interval-ms <ms> State refresh interval. Defaults to 1000",
|
|
1101
|
+
" --timeout-ms <ms> OAuth callback timeout. Defaults to 600000",
|
|
1102
|
+
"",
|
|
1103
|
+
"Environment:",
|
|
1104
|
+
" RUNNETH_API_URL, RUNNETH_RESOURCE_URL, RUNNETH_WORKSPACE_ID",
|
|
1105
|
+
" RUNNETH_TOKEN, MONDRIAN_TOKEN, BUILDER_AGENT_CHAT_TOKEN",
|
|
1106
|
+
"",
|
|
1107
|
+
"Commands inside chat:",
|
|
1108
|
+
" /state Redraw the current conversation state",
|
|
1109
|
+
" /exit Quit",
|
|
1110
|
+
""
|
|
1111
|
+
].join("\n"));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
await runRunnethConversation(await resolveRunnethConversationOptions(parseRunnethConversationArgs(argv.slice(1), parseNumber)));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (command === "conversation") {
|
|
1118
|
+
await runRunnethConversationMachineCommand({
|
|
1119
|
+
argv: argv.slice(1),
|
|
1120
|
+
parseNumber
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (command === "oauth") {
|
|
1125
|
+
const subcommand = argv[1];
|
|
1126
|
+
if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
|
|
1127
|
+
process$1.stdout.write([
|
|
1128
|
+
"Usage: runneth-cli oauth <login|status|logout> [--resource <url>] [options]",
|
|
1129
|
+
"",
|
|
1130
|
+
"Options:",
|
|
1131
|
+
" --client-name <name> Dynamic OAuth client name",
|
|
1132
|
+
" --no-open Print the authorization URL without opening a browser",
|
|
1133
|
+
" --resource <url> OAuth-protected MCP resource URL, defaults to build MONDRIAN_API_URL/mcp",
|
|
1134
|
+
" --scope <scope> OAuth scopes",
|
|
1135
|
+
" --timeout-ms <ms> Authorization callback timeout",
|
|
1136
|
+
""
|
|
1137
|
+
].join("\n"));
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const parsed = parseOAuthArgs(argv.slice(2));
|
|
1141
|
+
const resourceUrl = resolveOAuthResourceUrl(parsed);
|
|
1142
|
+
if (subcommand === "login") {
|
|
1143
|
+
const result = await loginWithOAuth({
|
|
1144
|
+
authorizationUrlHandler: (authorizationUrl) => {
|
|
1145
|
+
process$1.stderr.write(`Authorize runneth-cli here:\n${authorizationUrl}\n`);
|
|
1146
|
+
},
|
|
1147
|
+
clientName: parsed.clientName,
|
|
1148
|
+
openBrowser: parsed.openBrowser,
|
|
1149
|
+
resourceUrl,
|
|
1150
|
+
scope: parsed.scope,
|
|
1151
|
+
timeoutMs: parsed.timeoutMs
|
|
1152
|
+
});
|
|
1153
|
+
writeJson({
|
|
1154
|
+
accessToken: result.accessToken,
|
|
1155
|
+
credentialPath: result.credentialPath,
|
|
1156
|
+
expiresAt: result.expiresAt,
|
|
1157
|
+
ok: true,
|
|
1158
|
+
op: "oauth-login",
|
|
1159
|
+
resource: result.credential.resource,
|
|
1160
|
+
scope: result.credential.token.scope ?? result.credential.scope,
|
|
1161
|
+
tokenType: result.tokenType
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (subcommand === "status") {
|
|
1166
|
+
writeJson({
|
|
1167
|
+
ok: true,
|
|
1168
|
+
op: "oauth-status",
|
|
1169
|
+
...await readOAuthCredentialStatus({ resourceUrl })
|
|
1170
|
+
});
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (subcommand === "logout") {
|
|
1174
|
+
writeJson({
|
|
1175
|
+
ok: true,
|
|
1176
|
+
op: "oauth-logout",
|
|
1177
|
+
...await logoutOAuthCredential({ resourceUrl })
|
|
1178
|
+
});
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
throw new Error(`Unknown oauth command: ${subcommand}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (command === "copy") {
|
|
1184
|
+
if (argv[1] === "--help" || argv[1] === "-h") {
|
|
1185
|
+
process$1.stdout.write([
|
|
1186
|
+
"Usage: runneth-cli copy --from <source-target> --to <destination-target> <source-path> <destination-path> [options]",
|
|
1187
|
+
"",
|
|
1188
|
+
"Copies one absolute VM path to another VM through Builder file-share upload.",
|
|
1189
|
+
"",
|
|
1190
|
+
"Options:",
|
|
1191
|
+
" --from <target> Source SSH target name",
|
|
1192
|
+
" --to <target> Destination SSH target name",
|
|
1193
|
+
" --ignore <path> Additional relative folder/path to skip; node_modules is skipped by default",
|
|
1194
|
+
" --client-name <name> Dynamic OAuth client name",
|
|
1195
|
+
" --no-open Print the authorization URL without opening a browser",
|
|
1196
|
+
" --scope <scope> OAuth scopes",
|
|
1197
|
+
" --timeout-ms <ms> Copy command timeout",
|
|
1198
|
+
"",
|
|
1199
|
+
"Examples:",
|
|
1200
|
+
" runneth-cli copy --from source-vm --to destination-vm /agent/output.tgz /agent/imports/output.tgz",
|
|
1201
|
+
""
|
|
1202
|
+
].join("\n"));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const parsed = parseCopyArgs(argv.slice(1));
|
|
1206
|
+
const sourcePath = parsed.values[0];
|
|
1207
|
+
const destinationPath = parsed.values[1];
|
|
1208
|
+
if (sourcePath === void 0 || destinationPath === void 0 || parsed.values.length !== 2) throw new Error("runneth-cli copy requires exactly <source-path> and <destination-path>");
|
|
1209
|
+
const sourceTarget = await resolveRunnethSshTarget({ targetName: parsed.sourceTargetName });
|
|
1210
|
+
const destinationTarget = await resolveRunnethSshTarget({ targetName: parsed.destinationTargetName });
|
|
1211
|
+
assertRunnethVmCopyTargetsUseSameResource({
|
|
1212
|
+
destination: destinationTarget,
|
|
1213
|
+
source: sourceTarget
|
|
1214
|
+
});
|
|
1215
|
+
const oauthToken = await resolveSshOAuthToken(parsed, sourceTarget.resourceUrl);
|
|
1216
|
+
writeJson({
|
|
1217
|
+
ok: true,
|
|
1218
|
+
op: "copy",
|
|
1219
|
+
...await runRunnethVmCopy({
|
|
1220
|
+
destination: destinationTarget,
|
|
1221
|
+
destinationPath,
|
|
1222
|
+
ignoredPaths: parsed.ignoredPaths,
|
|
1223
|
+
oauthToken,
|
|
1224
|
+
source: sourceTarget,
|
|
1225
|
+
sourcePath,
|
|
1226
|
+
timeoutMs: parsed.timeoutMs
|
|
1227
|
+
})
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (command === "skills") {
|
|
1232
|
+
const subcommand = argv[1];
|
|
1233
|
+
if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
|
|
1234
|
+
process$1.stdout.write([
|
|
1235
|
+
"Usage: runneth-cli skills <install> [--agent claude|codex|all]",
|
|
1236
|
+
"",
|
|
1237
|
+
"Commands:",
|
|
1238
|
+
" install Install or update the bundled Runneth skill",
|
|
1239
|
+
"",
|
|
1240
|
+
"Options:",
|
|
1241
|
+
" --agent <agent> claude, codex, or all. Defaults to all",
|
|
1242
|
+
"",
|
|
1243
|
+
"Examples:",
|
|
1244
|
+
" runneth-cli skills install",
|
|
1245
|
+
" runneth-cli skills install --agent claude",
|
|
1246
|
+
" runneth-cli skills install --agent codex",
|
|
1247
|
+
""
|
|
1248
|
+
].join("\n"));
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (subcommand === "install") {
|
|
1252
|
+
writeJson({
|
|
1253
|
+
ok: true,
|
|
1254
|
+
op: "skills-install",
|
|
1255
|
+
...await installRunnethSkills({ agents: parseSkillsArgs(argv.slice(2)).agents })
|
|
1256
|
+
});
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
throw new Error(`Unknown skills command: ${subcommand}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (command === "ssh") {
|
|
1262
|
+
const subcommand = argv[1];
|
|
1263
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
1264
|
+
process$1.stdout.write([
|
|
1265
|
+
"Usage: runneth-cli ssh [stdio|target] [--target <name>] [--resource <url>] [--ssh-url <url>] [-- remote-command]",
|
|
1266
|
+
"",
|
|
1267
|
+
"Commands:",
|
|
1268
|
+
" ssh Log in if needed, install the public key, then run OpenSSH",
|
|
1269
|
+
" stdio Keep one SSH master open and accept JSONL exec/process requests on stdin",
|
|
1270
|
+
" target Manage named Runneth SSH VM targets",
|
|
1271
|
+
"",
|
|
1272
|
+
"Options:",
|
|
1273
|
+
" --client-name <name> Dynamic OAuth client name",
|
|
1274
|
+
" --no-open Print the authorization URL without opening a browser",
|
|
1275
|
+
" --resource <url> OAuth-protected MCP resource URL, defaults to build MONDRIAN_API_URL/mcp when using --host or --ssh-url",
|
|
1276
|
+
" --scope <scope> OAuth scopes",
|
|
1277
|
+
" --ssh-url <url> SSH app URL, defaults to <resource-origin>/runneth/ssh",
|
|
1278
|
+
" --target <name> Stored SSH target name",
|
|
1279
|
+
" --timeout-ms <ms> Authorization callback timeout",
|
|
1280
|
+
" --host <host> Build SSH URL as https://<host>/runneth/ssh",
|
|
1281
|
+
" --identity-file <path> Use an existing private key and matching .pub file",
|
|
1282
|
+
" --unique-key Generate/use a per-target key instead of the shared key",
|
|
1283
|
+
"",
|
|
1284
|
+
"Target commands:",
|
|
1285
|
+
" runneth-cli ssh target add <name> [--resource <url>] (--ssh-url <url> | --host <host>) [--default]",
|
|
1286
|
+
" runneth-cli ssh target import <file> [--resource <url>] [--default]",
|
|
1287
|
+
" runneth-cli ssh target list",
|
|
1288
|
+
" runneth-cli ssh target use <name>",
|
|
1289
|
+
" runneth-cli ssh target remove <name>",
|
|
1290
|
+
"",
|
|
1291
|
+
"Examples:",
|
|
1292
|
+
" runneth-cli ssh target add primary-vm --host 93c7ca56-debe-4b95-8be2-a873afe72234.app.runneth.com --default",
|
|
1293
|
+
" runneth-cli ssh -- 'pwd'",
|
|
1294
|
+
" runneth-cli ssh --target primary-vm -- 'pwd'",
|
|
1295
|
+
" runneth-cli ssh stdio --target primary-vm",
|
|
1296
|
+
""
|
|
1297
|
+
].join("\n"));
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (subcommand === "target") {
|
|
1301
|
+
const targetCommand = argv[2];
|
|
1302
|
+
if (targetCommand === void 0 || targetCommand === "--help" || targetCommand === "-h") {
|
|
1303
|
+
process$1.stdout.write([
|
|
1304
|
+
"Usage: runneth-cli ssh target <add|import|list|use|remove> [options]",
|
|
1305
|
+
"",
|
|
1306
|
+
"Commands:",
|
|
1307
|
+
" add <name> Add or update a named SSH target",
|
|
1308
|
+
" import <file> Add or update SSH targets from a JSON file",
|
|
1309
|
+
" list List SSH targets",
|
|
1310
|
+
" use <name> Set the default SSH target",
|
|
1311
|
+
" remove <name> Remove an SSH target",
|
|
1312
|
+
"",
|
|
1313
|
+
"Add options:",
|
|
1314
|
+
" --default Set this target as default",
|
|
1315
|
+
" --resource <url> OAuth-protected MCP resource URL, defaults to build MONDRIAN_API_URL/mcp",
|
|
1316
|
+
" --ssh-url <url> SSH app URL",
|
|
1317
|
+
" --host <host> Build SSH URL as https://<host>/runneth/ssh",
|
|
1318
|
+
"",
|
|
1319
|
+
"Import file:",
|
|
1320
|
+
" { \"defaultTarget\": \"vm-one\", \"targets\": [{ \"name\": \"vm-one\", \"host\": \"vm-one.example.com\" }] }",
|
|
1321
|
+
""
|
|
1322
|
+
].join("\n"));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (targetCommand === "add") {
|
|
1326
|
+
const parsed = parseSshArgs(argv.slice(3));
|
|
1327
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh target add");
|
|
1328
|
+
const name = parsed.values[0];
|
|
1329
|
+
if (name === void 0 || parsed.values.length !== 1) throw new Error("runneth-cli ssh target add requires one target name");
|
|
1330
|
+
if (parsed.sshHost === void 0 && parsed.sshUrl === void 0) throw new Error("runneth-cli ssh target add requires --host <host> or --ssh-url <url>");
|
|
1331
|
+
if (parsed.targetName !== void 0) throw new Error("--target is not valid for ssh target add");
|
|
1332
|
+
const saved = await saveRunnethSshTarget({
|
|
1333
|
+
makeDefault: parsed.makeDefault,
|
|
1334
|
+
name,
|
|
1335
|
+
resourceUrl: requireResourceUrl(parsed.resourceUrl ?? resolveDefaultResourceUrl(), "runneth-cli ssh target add"),
|
|
1336
|
+
sshUrl: resolveSshUrlOption(parsed)
|
|
1337
|
+
});
|
|
1338
|
+
writeJson({
|
|
1339
|
+
defaultTarget: saved.store.defaultTarget,
|
|
1340
|
+
ok: true,
|
|
1341
|
+
op: "ssh-target-add",
|
|
1342
|
+
target: saved.target
|
|
1343
|
+
});
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (targetCommand === "import") {
|
|
1347
|
+
const parsed = parseSshArgs(argv.slice(3));
|
|
1348
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh target import");
|
|
1349
|
+
const filePath = parsed.values[0];
|
|
1350
|
+
if (filePath === void 0 || parsed.values.length !== 1) throw new Error("runneth-cli ssh target import requires one JSON file path");
|
|
1351
|
+
if (parsed.targetName !== void 0) throw new Error("--target is not valid for ssh target import");
|
|
1352
|
+
if (parsed.sshHost !== void 0 || parsed.sshUrl !== void 0) throw new Error("runneth-cli ssh target import reads host and sshUrl from the file");
|
|
1353
|
+
const importFile = await readSshTargetImportFile(filePath);
|
|
1354
|
+
if (parsed.makeDefault && importFile.defaultTarget !== void 0) throw new Error("Use either import file defaultTarget or --default, not both");
|
|
1355
|
+
const entries = resolveSshTargetImportEntries({
|
|
1356
|
+
...parsed.resourceUrl === void 0 ? {} : { commandResourceUrl: parsed.resourceUrl },
|
|
1357
|
+
file: importFile,
|
|
1358
|
+
makeFirstDefault: parsed.makeDefault
|
|
1359
|
+
});
|
|
1360
|
+
const targets = [];
|
|
1361
|
+
let defaultTarget;
|
|
1362
|
+
for (const entry of entries) {
|
|
1363
|
+
const saved = await saveRunnethSshTarget({
|
|
1364
|
+
makeDefault: entry.makeDefault,
|
|
1365
|
+
name: entry.name,
|
|
1366
|
+
resourceUrl: entry.resourceUrl,
|
|
1367
|
+
sshUrl: entry.sshUrl
|
|
1368
|
+
});
|
|
1369
|
+
targets.push(saved.target);
|
|
1370
|
+
defaultTarget = saved.store.defaultTarget;
|
|
1371
|
+
}
|
|
1372
|
+
writeJson({
|
|
1373
|
+
defaultTarget,
|
|
1374
|
+
ok: true,
|
|
1375
|
+
op: "ssh-target-import",
|
|
1376
|
+
targets
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (targetCommand === "list") {
|
|
1381
|
+
const parsed = parseSshArgs(argv.slice(3));
|
|
1382
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh target list");
|
|
1383
|
+
if (parsed.values.length > 0) throw new Error("runneth-cli ssh target list does not accept names");
|
|
1384
|
+
const store = await readRunnethSshTargetStore({});
|
|
1385
|
+
writeJson({
|
|
1386
|
+
defaultTarget: store.defaultTarget,
|
|
1387
|
+
ok: true,
|
|
1388
|
+
op: "ssh-target-list",
|
|
1389
|
+
targets: store.targets
|
|
1390
|
+
});
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (targetCommand === "use") {
|
|
1394
|
+
const parsed = parseSshArgs(argv.slice(3));
|
|
1395
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh target use");
|
|
1396
|
+
const name = parsed.values[0];
|
|
1397
|
+
if (name === void 0 || parsed.values.length !== 1) throw new Error("runneth-cli ssh target use requires one target name");
|
|
1398
|
+
const store = await setDefaultRunnethSshTarget({ name });
|
|
1399
|
+
const target = store.targets.find((item) => item.name === name);
|
|
1400
|
+
writeJson({
|
|
1401
|
+
defaultTarget: store.defaultTarget,
|
|
1402
|
+
ok: true,
|
|
1403
|
+
op: "ssh-target-use",
|
|
1404
|
+
target
|
|
1405
|
+
});
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (targetCommand === "remove") {
|
|
1409
|
+
const parsed = parseSshArgs(argv.slice(3));
|
|
1410
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh target remove");
|
|
1411
|
+
const name = parsed.values[0];
|
|
1412
|
+
if (name === void 0 || parsed.values.length !== 1) throw new Error("runneth-cli ssh target remove requires one target name");
|
|
1413
|
+
const store = await removeRunnethSshTarget({ name });
|
|
1414
|
+
writeJson({
|
|
1415
|
+
defaultTarget: store.defaultTarget,
|
|
1416
|
+
ok: true,
|
|
1417
|
+
op: "ssh-target-remove",
|
|
1418
|
+
targetName: name,
|
|
1419
|
+
targets: store.targets
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
throw new Error(`Unknown ssh target command: ${targetCommand}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (subcommand === "proxy") {
|
|
1426
|
+
const parsed = parseSshArgs(argv.slice(2));
|
|
1427
|
+
if (parsed.values.length > 0) throw new Error("runneth-cli ssh proxy does not accept extra arguments");
|
|
1428
|
+
assertNoSshTargetDefaultFlag(parsed);
|
|
1429
|
+
assertNoSshKeyOptions(parsed, "runneth-cli ssh proxy");
|
|
1430
|
+
const target = await resolveSshTargetForCommand(parsed);
|
|
1431
|
+
await runRunnethSshProxy({
|
|
1432
|
+
resourceUrl: target.resourceUrl,
|
|
1433
|
+
sshUrl: target.sshUrl
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (subcommand === "stdio") {
|
|
1438
|
+
const parsed = parseSshArgs(argv.slice(2));
|
|
1439
|
+
if (parsed.values.length > 0) throw new Error("runneth-cli ssh stdio does not accept extra arguments");
|
|
1440
|
+
assertNoSshTargetDefaultFlag(parsed);
|
|
1441
|
+
const target = await resolveSshTargetForCommand(parsed);
|
|
1442
|
+
const oauthToken = await resolveSshOAuthToken(parsed, target.resourceUrl);
|
|
1443
|
+
const setup = await installRunnethSshAccess({
|
|
1444
|
+
cliPath: fileURLToPath(import.meta.url),
|
|
1445
|
+
oauthToken,
|
|
1446
|
+
resourceUrl: target.resourceUrl,
|
|
1447
|
+
sshUrl: target.sshUrl,
|
|
1448
|
+
...resolveSshKeyInstallOptions(parsed),
|
|
1449
|
+
...target.targetName === void 0 ? {} : { targetName: target.targetName }
|
|
1450
|
+
});
|
|
1451
|
+
process$1.stderr.write(`SSH target ${setup.hostAlias} configured with ${setup.configPath}\n`);
|
|
1452
|
+
await runRunnethSshStdio({
|
|
1453
|
+
defaultTimeoutMs: parsed.timeoutMs,
|
|
1454
|
+
setup
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const parsed = parseSshArgs(argv.slice(1));
|
|
1459
|
+
assertNoSshTargetDefaultFlag(parsed);
|
|
1460
|
+
assertSshRemoteCommandSeparator(parsed);
|
|
1461
|
+
const target = await resolveSshTargetForCommand(parsed);
|
|
1462
|
+
const oauthToken = await resolveSshOAuthToken(parsed, target.resourceUrl);
|
|
1463
|
+
const setup = await installRunnethSshAccess({
|
|
1464
|
+
cliPath: fileURLToPath(import.meta.url),
|
|
1465
|
+
oauthToken,
|
|
1466
|
+
resourceUrl: target.resourceUrl,
|
|
1467
|
+
sshUrl: target.sshUrl,
|
|
1468
|
+
...resolveSshKeyInstallOptions(parsed),
|
|
1469
|
+
...target.targetName === void 0 ? {} : { targetName: target.targetName }
|
|
1470
|
+
});
|
|
1471
|
+
process$1.stderr.write(`SSH target ${setup.hostAlias} configured with ${setup.configPath}\n`);
|
|
1472
|
+
process$1.exitCode = await runOpenSsh({
|
|
1473
|
+
extraArgs: parsed.values,
|
|
1474
|
+
setup
|
|
1475
|
+
});
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (command === "close") {
|
|
1479
|
+
writeJson(await request({
|
|
1480
|
+
name: parseArgs(argv.slice(1)).name,
|
|
1481
|
+
op: "close"
|
|
1482
|
+
}));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (command === "shutdown") {
|
|
1486
|
+
writeJson(await request({ op: "shutdown" }));
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1490
|
+
};
|
|
1491
|
+
const isSshProxyInvocation = (argv) => {
|
|
1492
|
+
return argv[0] === "ssh" && argv[1] === "proxy";
|
|
1493
|
+
};
|
|
1494
|
+
const isSshStdioInvocation = (argv) => {
|
|
1495
|
+
return argv[0] === "ssh" && argv[1] === "stdio";
|
|
1496
|
+
};
|
|
1497
|
+
const isConversationSessionInvocation = (argv) => {
|
|
1498
|
+
return argv[0] === "conversation" && argv[1] === "session";
|
|
1499
|
+
};
|
|
1500
|
+
const argv = process$1.argv.slice(2);
|
|
1501
|
+
run(argv).catch((error) => {
|
|
1502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1503
|
+
if (isSshProxyInvocation(argv)) {
|
|
1504
|
+
process$1.stderr.write(`${message}\n`);
|
|
1505
|
+
process$1.exitCode = 1;
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (isSshStdioInvocation(argv)) {
|
|
1509
|
+
process$1.stdout.write(`${JSON.stringify({
|
|
1510
|
+
message,
|
|
1511
|
+
ok: false,
|
|
1512
|
+
type: "error"
|
|
1513
|
+
})}\n`);
|
|
1514
|
+
process$1.exitCode = 1;
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (isConversationSessionInvocation(argv)) {
|
|
1518
|
+
process$1.stdout.write(`${JSON.stringify({
|
|
1519
|
+
message,
|
|
1520
|
+
ok: false,
|
|
1521
|
+
type: "error"
|
|
1522
|
+
})}\n`);
|
|
1523
|
+
process$1.exitCode = 1;
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
process$1.stdout.write(`${JSON.stringify({
|
|
1527
|
+
error: message,
|
|
1528
|
+
ok: false
|
|
1529
|
+
}, null, 2)}\n`);
|
|
1530
|
+
process$1.exitCode = 1;
|
|
1531
|
+
});
|
|
1532
|
+
//#endregion
|
|
1533
|
+
export {};
|