@runneth/cli 0.0.0-sha.3142666bf4dc.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/copy.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OAuthTokenResult } from "./oauth.js";
|
|
2
|
+
import type { ResolvedRunnethSshTarget } from "./ssh.js";
|
|
3
|
+
export type RunnethVmCopyOptions = Readonly<{
|
|
4
|
+
readonly destination: ResolvedRunnethSshTarget;
|
|
5
|
+
readonly destinationPath: string;
|
|
6
|
+
readonly ignoredPaths?: readonly string[];
|
|
7
|
+
readonly oauthToken: OAuthTokenResult;
|
|
8
|
+
readonly source: ResolvedRunnethSshTarget;
|
|
9
|
+
readonly sourcePath: string;
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
}>;
|
|
12
|
+
export type RunnethVmCopyResult = Readonly<{
|
|
13
|
+
readonly bytesCopied: number;
|
|
14
|
+
readonly destinationPath: string;
|
|
15
|
+
readonly destinationTargetName?: string;
|
|
16
|
+
readonly destinationVmName: string;
|
|
17
|
+
readonly sourcePath: string;
|
|
18
|
+
readonly sourceTargetName?: string;
|
|
19
|
+
readonly sourceVmName: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const assertRunnethVmCopyTargetsUseSameResource: (input: {
|
|
22
|
+
readonly destination: ResolvedRunnethSshTarget;
|
|
23
|
+
readonly source: ResolvedRunnethSshTarget;
|
|
24
|
+
}) => void;
|
|
25
|
+
export declare const runRunnethVmCopy: (options: RunnethVmCopyOptions) => Promise<RunnethVmCopyResult>;
|
package/dist/copy.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const DEFAULT_IGNORED_PATHS = ["node_modules"];
|
|
3
|
+
const DESTINATION_CLEANUP_TIMEOUT_MS = 30_000;
|
|
4
|
+
const SSH_APP_ARCHIVE_PATH = "/api/files/archive";
|
|
5
|
+
const SSH_APP_ARCHIVE_APPLY_PATH = "/api/files/archive/apply";
|
|
6
|
+
const SSH_APP_ARCHIVE_UPLOAD_CLEANUP_PATH = "/api/files/archive/uploads/cleanup";
|
|
7
|
+
const SSH_APP_ARCHIVE_UPLOAD_PREPARE_PATH = "/api/files/archive/uploads/prepare";
|
|
8
|
+
const SPAWNBOX_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
9
|
+
const isRecord = (value) => {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
};
|
|
12
|
+
const normalizeResourceUrl = (value) => {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
url.hash = "";
|
|
15
|
+
url.search = "";
|
|
16
|
+
if (url.pathname !== "/") {
|
|
17
|
+
url.pathname = url.pathname.replace(/\/+$/u, "");
|
|
18
|
+
}
|
|
19
|
+
return url.toString();
|
|
20
|
+
};
|
|
21
|
+
export const assertRunnethVmCopyTargetsUseSameResource = (input) => {
|
|
22
|
+
if (normalizeResourceUrl(input.source.resourceUrl) !==
|
|
23
|
+
normalizeResourceUrl(input.destination.resourceUrl)) {
|
|
24
|
+
throw new Error("Source and destination targets must use the same resource");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const normalizeRemotePath = (value, label) => {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (trimmed.length === 0) {
|
|
30
|
+
throw new Error(`${label} must be a non-empty absolute path`);
|
|
31
|
+
}
|
|
32
|
+
if (trimmed.includes("\0")) {
|
|
33
|
+
throw new Error(`${label} must not contain NUL bytes`);
|
|
34
|
+
}
|
|
35
|
+
const normalized = path.posix.normalize(trimmed);
|
|
36
|
+
if (!normalized.startsWith("/")) {
|
|
37
|
+
throw new Error(`${label} must be an absolute path`);
|
|
38
|
+
}
|
|
39
|
+
if (normalized === "/") {
|
|
40
|
+
throw new Error(`${label} must not be the filesystem root`);
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
};
|
|
44
|
+
const normalizeIgnoredPath = (value) => {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
if (trimmed.length === 0) {
|
|
47
|
+
throw new Error("Ignored path must be non-empty");
|
|
48
|
+
}
|
|
49
|
+
if (trimmed.includes("\0")) {
|
|
50
|
+
throw new Error("Ignored path must not contain NUL bytes");
|
|
51
|
+
}
|
|
52
|
+
if (path.posix.isAbsolute(trimmed)) {
|
|
53
|
+
throw new Error("Ignored path must be relative");
|
|
54
|
+
}
|
|
55
|
+
const normalized = path.posix.normalize(trimmed);
|
|
56
|
+
if (normalized === "." ||
|
|
57
|
+
normalized === ".." ||
|
|
58
|
+
normalized.startsWith("../")) {
|
|
59
|
+
throw new Error("Ignored path must stay within the copied path");
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
};
|
|
63
|
+
const resolveIgnoredPaths = (ignoredPaths) => {
|
|
64
|
+
const normalized = new Set();
|
|
65
|
+
for (const ignoredPath of DEFAULT_IGNORED_PATHS) {
|
|
66
|
+
normalized.add(normalizeIgnoredPath(ignoredPath));
|
|
67
|
+
}
|
|
68
|
+
for (const ignoredPath of ignoredPaths ?? []) {
|
|
69
|
+
normalized.add(normalizeIgnoredPath(ignoredPath));
|
|
70
|
+
}
|
|
71
|
+
return [...normalized].sort();
|
|
72
|
+
};
|
|
73
|
+
const resolveBearerAuthorizationHeader = (token) => {
|
|
74
|
+
if (token.tokenType.toLowerCase() !== "bearer") {
|
|
75
|
+
throw new Error(`Unsupported OAuth token type for copy: ${token.tokenType}`);
|
|
76
|
+
}
|
|
77
|
+
return `Bearer ${token.accessToken}`;
|
|
78
|
+
};
|
|
79
|
+
const buildSshAppApiUrl = (sshUrl, apiPath) => {
|
|
80
|
+
const url = new URL(sshUrl);
|
|
81
|
+
url.pathname = `${url.pathname.replace(/\/+$/u, "")}${apiPath}`;
|
|
82
|
+
url.search = "";
|
|
83
|
+
url.hash = "";
|
|
84
|
+
return url;
|
|
85
|
+
};
|
|
86
|
+
const buildSshAppArchiveUrl = (sshUrl, remotePath, ignoredPaths) => {
|
|
87
|
+
const url = buildSshAppApiUrl(sshUrl, SSH_APP_ARCHIVE_PATH);
|
|
88
|
+
url.searchParams.set("path", remotePath);
|
|
89
|
+
for (const ignoredPath of ignoredPaths) {
|
|
90
|
+
url.searchParams.append("ignore", ignoredPath);
|
|
91
|
+
}
|
|
92
|
+
return url.toString();
|
|
93
|
+
};
|
|
94
|
+
const resolveTargetVmName = (target, label) => {
|
|
95
|
+
const host = new URL(target.sshUrl).hostname;
|
|
96
|
+
const firstLabel = host.split(".")[0] ?? "";
|
|
97
|
+
if (SPAWNBOX_ID_PATTERN.test(firstLabel)) {
|
|
98
|
+
return firstLabel;
|
|
99
|
+
}
|
|
100
|
+
if (target.targetName !== undefined) {
|
|
101
|
+
return target.targetName;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Cannot derive ${label} VM name from SSH URL host: ${host}`);
|
|
104
|
+
};
|
|
105
|
+
const parseNumberField = (record, key) => {
|
|
106
|
+
const value = record[key];
|
|
107
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
108
|
+
throw new Error(`Copy response ${key} must be a non-negative number`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
const parseArchiveUploadResponse = (payload) => {
|
|
113
|
+
if (!isRecord(payload) || payload.ok !== true) {
|
|
114
|
+
throw new Error("Archive upload response must be an ok JSON object");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
bytesWritten: parseNumberField(payload, "bytesWritten"),
|
|
118
|
+
entriesWritten: parseNumberField(payload, "entriesWritten"),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
const parsePreparedUploadHeaders = (payload) => {
|
|
122
|
+
if (!isRecord(payload)) {
|
|
123
|
+
throw new Error("Prepared upload headers must be a JSON object");
|
|
124
|
+
}
|
|
125
|
+
const headers = {};
|
|
126
|
+
for (const [headerName, headerValue] of Object.entries(payload)) {
|
|
127
|
+
if (typeof headerValue !== "string") {
|
|
128
|
+
throw new Error("Prepared upload headers must be string values");
|
|
129
|
+
}
|
|
130
|
+
headers[headerName] = headerValue;
|
|
131
|
+
}
|
|
132
|
+
return headers;
|
|
133
|
+
};
|
|
134
|
+
const parseAzureFileSharePreparedUpload = (payload) => {
|
|
135
|
+
if (!isRecord(payload)) {
|
|
136
|
+
throw new Error("Prepared upload must be a JSON object");
|
|
137
|
+
}
|
|
138
|
+
if (payload.strategy !== "azure_file_share" || payload.method !== "PUT") {
|
|
139
|
+
throw new Error("Prepared upload must be an Azure File Share PUT upload");
|
|
140
|
+
}
|
|
141
|
+
if (typeof payload.url !== "string" || payload.url.length === 0) {
|
|
142
|
+
throw new Error("Prepared upload url must be a non-empty string");
|
|
143
|
+
}
|
|
144
|
+
if (typeof payload.rangeSize !== "number" ||
|
|
145
|
+
!Number.isInteger(payload.rangeSize) ||
|
|
146
|
+
payload.rangeSize <= 0) {
|
|
147
|
+
throw new Error("Prepared upload rangeSize must be a positive integer");
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
headers: parsePreparedUploadHeaders(payload.headers),
|
|
151
|
+
method: "PUT",
|
|
152
|
+
rangeSize: payload.rangeSize,
|
|
153
|
+
strategy: "azure_file_share",
|
|
154
|
+
url: payload.url,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const parseArchiveUploadPrepareResponse = (payload) => {
|
|
158
|
+
if (!isRecord(payload) || payload.ok !== true) {
|
|
159
|
+
throw new Error("Archive upload prepare response must be an ok JSON object");
|
|
160
|
+
}
|
|
161
|
+
if (typeof payload.archivePath !== "string" || payload.archivePath.length === 0) {
|
|
162
|
+
throw new Error("Archive upload prepare response archivePath must be a non-empty string");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
archivePath: payload.archivePath,
|
|
166
|
+
upload: parseAzureFileSharePreparedUpload(payload.upload),
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
const parseResponseHeaderNumber = (headers, name) => {
|
|
170
|
+
const value = headers.get(name);
|
|
171
|
+
if (value === null) {
|
|
172
|
+
throw new Error(`Archive download response missing ${name}`);
|
|
173
|
+
}
|
|
174
|
+
const parsed = Number(value);
|
|
175
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
176
|
+
throw new Error(`Archive download response ${name} must be a non-negative integer`);
|
|
177
|
+
}
|
|
178
|
+
return parsed;
|
|
179
|
+
};
|
|
180
|
+
const downloadSourceArchive = async (input) => {
|
|
181
|
+
const response = await fetch(buildSshAppArchiveUrl(input.sourceSshUrl, input.sourcePath, input.ignoredPaths), {
|
|
182
|
+
headers: {
|
|
183
|
+
Accept: "application/vnd.runneth.file-archive",
|
|
184
|
+
Authorization: input.authorizationHeader,
|
|
185
|
+
},
|
|
186
|
+
method: "GET",
|
|
187
|
+
signal: input.signal,
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Runneth VM source download failed with HTTP ${String(response.status)}: ${await response.text()}`);
|
|
191
|
+
}
|
|
192
|
+
if (response.body === null) {
|
|
193
|
+
throw new Error("Runneth VM source download response body is missing");
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
archiveByteLength: parseResponseHeaderNumber(response.headers, "content-length"),
|
|
197
|
+
bytesArchived: parseResponseHeaderNumber(response.headers, "x-runneth-archive-bytes"),
|
|
198
|
+
entriesArchived: parseResponseHeaderNumber(response.headers, "x-runneth-archive-entries"),
|
|
199
|
+
stream: response.body,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
const prepareDestinationArchiveUpload = async (input) => {
|
|
203
|
+
const response = await fetch(buildSshAppApiUrl(input.destinationSshUrl, SSH_APP_ARCHIVE_UPLOAD_PREPARE_PATH), {
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
destinationPath: input.destinationPath,
|
|
206
|
+
size: input.archiveByteLength,
|
|
207
|
+
}),
|
|
208
|
+
headers: {
|
|
209
|
+
Accept: "application/json",
|
|
210
|
+
Authorization: input.authorizationHeader,
|
|
211
|
+
"Content-Type": "application/json",
|
|
212
|
+
},
|
|
213
|
+
method: "POST",
|
|
214
|
+
signal: input.signal,
|
|
215
|
+
});
|
|
216
|
+
const responseBody = await response.text();
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw new Error(`Runneth VM destination upload prepare failed with HTTP ${String(response.status)}: ${responseBody}`);
|
|
219
|
+
}
|
|
220
|
+
return parseArchiveUploadPrepareResponse(JSON.parse(responseBody));
|
|
221
|
+
};
|
|
222
|
+
const buildAzureFileShareRangeUploadUrl = (uploadUrl) => {
|
|
223
|
+
const url = new URL(uploadUrl);
|
|
224
|
+
url.searchParams.set("comp", "range");
|
|
225
|
+
return url.toString();
|
|
226
|
+
};
|
|
227
|
+
const combineUploadChunks = (chunks, byteLength) => {
|
|
228
|
+
if (chunks.length === 1 && chunks[0]?.byteLength === byteLength) {
|
|
229
|
+
return chunks[0];
|
|
230
|
+
}
|
|
231
|
+
const combined = new Uint8Array(byteLength);
|
|
232
|
+
let offset = 0;
|
|
233
|
+
for (const chunk of chunks) {
|
|
234
|
+
combined.set(chunk, offset);
|
|
235
|
+
offset += chunk.byteLength;
|
|
236
|
+
}
|
|
237
|
+
return combined;
|
|
238
|
+
};
|
|
239
|
+
async function* readUploadChunks(input) {
|
|
240
|
+
const reader = input.stream.getReader();
|
|
241
|
+
let pending = [];
|
|
242
|
+
let pendingByteLength = 0;
|
|
243
|
+
let offset = 0;
|
|
244
|
+
try {
|
|
245
|
+
while (offset < input.size) {
|
|
246
|
+
if (input.signal.aborted) {
|
|
247
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
248
|
+
}
|
|
249
|
+
const targetByteLength = Math.min(input.rangeSize, input.size - offset);
|
|
250
|
+
while (pendingByteLength < targetByteLength) {
|
|
251
|
+
const next = await reader.read();
|
|
252
|
+
if (next.done) {
|
|
253
|
+
throw new Error("Source archive stream ended before declared size");
|
|
254
|
+
}
|
|
255
|
+
if (next.value.byteLength > 0) {
|
|
256
|
+
pending = [...pending, next.value];
|
|
257
|
+
pendingByteLength += next.value.byteLength;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const combined = combineUploadChunks(pending, pendingByteLength);
|
|
261
|
+
const bytes = combined.subarray(0, targetByteLength);
|
|
262
|
+
const remainder = combined.subarray(targetByteLength);
|
|
263
|
+
const endExclusive = offset + targetByteLength;
|
|
264
|
+
yield {
|
|
265
|
+
bytes,
|
|
266
|
+
endExclusive,
|
|
267
|
+
offset,
|
|
268
|
+
};
|
|
269
|
+
pending = remainder.byteLength === 0 ? [] : [remainder];
|
|
270
|
+
pendingByteLength = remainder.byteLength;
|
|
271
|
+
offset = endExclusive;
|
|
272
|
+
}
|
|
273
|
+
if (pendingByteLength > 0) {
|
|
274
|
+
throw new Error("Source archive stream exceeded declared size");
|
|
275
|
+
}
|
|
276
|
+
const next = await reader.read();
|
|
277
|
+
if (!next.done && next.value.byteLength > 0) {
|
|
278
|
+
throw new Error("Source archive stream exceeded declared size");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
reader.releaseLock();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const uploadArchiveToAzureFileShare = async (input) => {
|
|
286
|
+
const rangeUrl = buildAzureFileShareRangeUploadUrl(input.upload.url);
|
|
287
|
+
const baseHeaders = new Headers(input.upload.headers);
|
|
288
|
+
baseHeaders.set("x-ms-write", "update");
|
|
289
|
+
for await (const chunk of readUploadChunks({
|
|
290
|
+
rangeSize: input.upload.rangeSize,
|
|
291
|
+
signal: input.signal,
|
|
292
|
+
size: input.archiveByteLength,
|
|
293
|
+
stream: input.stream,
|
|
294
|
+
})) {
|
|
295
|
+
const headers = new Headers(baseHeaders);
|
|
296
|
+
headers.set("content-length", String(chunk.bytes.byteLength));
|
|
297
|
+
headers.set("x-ms-range", `bytes=${String(chunk.offset)}-${String(chunk.endExclusive - 1)}`);
|
|
298
|
+
// Node fetch accepts typed arrays; this BodyInit type does not.
|
|
299
|
+
const requestBody = chunk.bytes;
|
|
300
|
+
const response = await fetch(rangeUrl, {
|
|
301
|
+
body: requestBody,
|
|
302
|
+
headers,
|
|
303
|
+
method: input.upload.method,
|
|
304
|
+
signal: input.signal,
|
|
305
|
+
});
|
|
306
|
+
const responseBody = await response.text();
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(`Runneth VM destination file-share upload failed with HTTP ${String(response.status)}: ${responseBody}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const applyDestinationArchive = async (input) => {
|
|
313
|
+
const response = await fetch(buildSshAppApiUrl(input.destinationSshUrl, SSH_APP_ARCHIVE_APPLY_PATH), {
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
archivePath: input.archivePath,
|
|
316
|
+
destinationPath: input.destinationPath,
|
|
317
|
+
}),
|
|
318
|
+
headers: {
|
|
319
|
+
Accept: "application/json",
|
|
320
|
+
Authorization: input.authorizationHeader,
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
},
|
|
323
|
+
method: "POST",
|
|
324
|
+
signal: input.signal,
|
|
325
|
+
});
|
|
326
|
+
const responseBody = await response.text();
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
throw new Error(`Runneth VM destination archive apply failed with HTTP ${String(response.status)}: ${responseBody}`);
|
|
329
|
+
}
|
|
330
|
+
return parseArchiveUploadResponse(JSON.parse(responseBody));
|
|
331
|
+
};
|
|
332
|
+
const cleanupDestinationArchiveUpload = async (input) => {
|
|
333
|
+
const response = await fetch(buildSshAppApiUrl(input.destinationSshUrl, SSH_APP_ARCHIVE_UPLOAD_CLEANUP_PATH), {
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
archivePath: input.archivePath,
|
|
336
|
+
}),
|
|
337
|
+
headers: {
|
|
338
|
+
Accept: "application/json",
|
|
339
|
+
Authorization: input.authorizationHeader,
|
|
340
|
+
"Content-Type": "application/json",
|
|
341
|
+
},
|
|
342
|
+
method: "POST",
|
|
343
|
+
signal: input.signal,
|
|
344
|
+
});
|
|
345
|
+
const responseBody = await response.text();
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`Runneth VM destination archive cleanup failed with HTTP ${String(response.status)}: ${responseBody}`);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const formatErrorMessage = (error) => {
|
|
351
|
+
return error instanceof Error ? error.message : String(error);
|
|
352
|
+
};
|
|
353
|
+
const assertUploadedByteCount = (input) => {
|
|
354
|
+
if (input.bytesWritten !== input.bytesArchived) {
|
|
355
|
+
throw new Error(`Runneth VM copy byte count mismatch: archived ${String(input.bytesArchived)}, wrote ${String(input.bytesWritten)}`);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const assertUploadedEntryCount = (input) => {
|
|
359
|
+
if (input.entriesWritten !== input.entriesArchived) {
|
|
360
|
+
throw new Error(`Runneth VM copy entry count mismatch: archived ${String(input.entriesArchived)}, wrote ${String(input.entriesWritten)}`);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const copyViaSshApps = async (input) => {
|
|
364
|
+
const archive = await downloadSourceArchive({
|
|
365
|
+
authorizationHeader: input.authorizationHeader,
|
|
366
|
+
ignoredPaths: input.ignoredPaths,
|
|
367
|
+
signal: input.signal,
|
|
368
|
+
sourcePath: input.sourcePath,
|
|
369
|
+
sourceSshUrl: input.sourceSshUrl,
|
|
370
|
+
});
|
|
371
|
+
let preparedUpload = null;
|
|
372
|
+
let copyCompleted = false;
|
|
373
|
+
let copyError = null;
|
|
374
|
+
try {
|
|
375
|
+
preparedUpload = await prepareDestinationArchiveUpload({
|
|
376
|
+
authorizationHeader: input.authorizationHeader,
|
|
377
|
+
archiveByteLength: archive.archiveByteLength,
|
|
378
|
+
destinationPath: input.destinationPath,
|
|
379
|
+
destinationSshUrl: input.destinationSshUrl,
|
|
380
|
+
signal: input.signal,
|
|
381
|
+
});
|
|
382
|
+
await uploadArchiveToAzureFileShare({
|
|
383
|
+
archiveByteLength: archive.archiveByteLength,
|
|
384
|
+
signal: input.signal,
|
|
385
|
+
stream: archive.stream,
|
|
386
|
+
upload: preparedUpload.upload,
|
|
387
|
+
});
|
|
388
|
+
const upload = await applyDestinationArchive({
|
|
389
|
+
archivePath: preparedUpload.archivePath,
|
|
390
|
+
authorizationHeader: input.authorizationHeader,
|
|
391
|
+
destinationPath: input.destinationPath,
|
|
392
|
+
destinationSshUrl: input.destinationSshUrl,
|
|
393
|
+
signal: input.signal,
|
|
394
|
+
});
|
|
395
|
+
assertUploadedByteCount({
|
|
396
|
+
bytesArchived: archive.bytesArchived,
|
|
397
|
+
bytesWritten: upload.bytesWritten,
|
|
398
|
+
});
|
|
399
|
+
assertUploadedEntryCount({
|
|
400
|
+
entriesArchived: archive.entriesArchived,
|
|
401
|
+
entriesWritten: upload.entriesWritten,
|
|
402
|
+
});
|
|
403
|
+
copyCompleted = true;
|
|
404
|
+
return upload.bytesWritten;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
copyError = error;
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
if (preparedUpload !== null && !copyCompleted) {
|
|
412
|
+
const cleanupAbortController = new AbortController();
|
|
413
|
+
const cleanupTimeout = setTimeout(() => {
|
|
414
|
+
cleanupAbortController.abort();
|
|
415
|
+
}, DESTINATION_CLEANUP_TIMEOUT_MS);
|
|
416
|
+
try {
|
|
417
|
+
await cleanupDestinationArchiveUpload({
|
|
418
|
+
archivePath: preparedUpload.archivePath,
|
|
419
|
+
authorizationHeader: input.authorizationHeader,
|
|
420
|
+
destinationSshUrl: input.destinationSshUrl,
|
|
421
|
+
signal: cleanupAbortController.signal,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (cleanupError) {
|
|
425
|
+
if (copyError !== null) {
|
|
426
|
+
throw new Error(`Runneth VM copy failed: ${formatErrorMessage(copyError)}; cleanup failed: ${formatErrorMessage(cleanupError)}`);
|
|
427
|
+
}
|
|
428
|
+
throw cleanupError;
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
clearTimeout(cleanupTimeout);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const runCopyRequest = async (input) => {
|
|
437
|
+
try {
|
|
438
|
+
return await copyViaSshApps(input);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
442
|
+
throw new Error("Runneth VM copy timed out");
|
|
443
|
+
}
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
export const runRunnethVmCopy = async (options) => {
|
|
448
|
+
assertRunnethVmCopyTargetsUseSameResource({
|
|
449
|
+
destination: options.destination,
|
|
450
|
+
source: options.source,
|
|
451
|
+
});
|
|
452
|
+
const sourcePath = normalizeRemotePath(options.sourcePath, "Source path");
|
|
453
|
+
const destinationPath = normalizeRemotePath(options.destinationPath, "Destination path");
|
|
454
|
+
const ignoredPaths = resolveIgnoredPaths(options.ignoredPaths);
|
|
455
|
+
const sourceVmName = resolveTargetVmName(options.source, "source");
|
|
456
|
+
const destinationVmName = resolveTargetVmName(options.destination, "destination");
|
|
457
|
+
const abortController = new AbortController();
|
|
458
|
+
const timeout = options.timeoutMs === undefined
|
|
459
|
+
? null
|
|
460
|
+
: setTimeout(() => {
|
|
461
|
+
abortController.abort();
|
|
462
|
+
}, options.timeoutMs);
|
|
463
|
+
try {
|
|
464
|
+
const bytesCopied = await runCopyRequest({
|
|
465
|
+
authorizationHeader: resolveBearerAuthorizationHeader(options.oauthToken),
|
|
466
|
+
destinationPath,
|
|
467
|
+
destinationSshUrl: options.destination.sshUrl,
|
|
468
|
+
ignoredPaths,
|
|
469
|
+
signal: abortController.signal,
|
|
470
|
+
sourcePath,
|
|
471
|
+
sourceSshUrl: options.source.sshUrl,
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
bytesCopied,
|
|
475
|
+
destinationPath,
|
|
476
|
+
destinationVmName,
|
|
477
|
+
...(options.destination.targetName === undefined
|
|
478
|
+
? {}
|
|
479
|
+
: { destinationTargetName: options.destination.targetName }),
|
|
480
|
+
sourcePath,
|
|
481
|
+
sourceVmName,
|
|
482
|
+
...(options.source.targetName === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { sourceTargetName: options.source.targetName }),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
if (timeout !== null) {
|
|
489
|
+
clearTimeout(timeout);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export { assertRunnethVmCopyTargetsUseSameResource, runRunnethVmCopy, type RunnethVmCopyOptions, type RunnethVmCopyResult, } from "./copy.js";
|
|
2
|
+
export { getOAuthAccessToken, loginWithOAuth, logoutOAuthCredential, readOAuthCredential, readOAuthCredentialStatus, resolveOAuthCredentialPath, type OAuthClientRegistration, type OAuthCredentialStatus, type OAuthLoginOptions, type OAuthServerMetadata, type OAuthStoredCredential, type OAuthStoredToken, type OAuthTokenEndpointAuthMethod, type OAuthTokenResult, } from "./oauth.js";
|
|
3
|
+
export { resolveRunnethCliHome, resolveSocketPath } from "./paths.js";
|
|
4
|
+
export { installRunnethSshAccess, normalizeRunnethSshTargetName, readRunnethSshTargetStore, removeRunnethSshTarget, resolveRunnethSshTarget, resolveRunnethSshAppUrl, resolveRunnethSshSharedKeyPaths, resolveRunnethSshTargetsPath, resolveRunnethSshTargetPaths, runOpenSsh, runRunnethSshProxy, saveRunnethSshTarget, setDefaultRunnethSshTarget, type OpenSshOptions, type ResolvedRunnethSshTarget, type RunnethSshAppMetadata, type RunnethSshGeneratedKeyMode, type RunnethSshInstallResponse, type RunnethSshKeyMode, type RunnethSshKeyPaths, type RunnethSshProxyOptions, type RunnethSshProxyStreams, type RunnethSshSetupOptions, type RunnethSshSetupResult, type RunnethSshTarget, type RunnethSshTargetPaths, type RunnethSshTargetStore, } from "./ssh.js";
|
|
5
|
+
export { installRunnethSkills, resolveBundledRunnethSkillPath, type RunnethSkillAgent, type RunnethSkillInstallOptions, type RunnethSkillInstallResult, type RunnethSkillInstallTarget, } from "./skills.js";
|
|
6
|
+
export type RunnethCliRequest = Readonly<{
|
|
7
|
+
cwd: string;
|
|
8
|
+
name: string;
|
|
9
|
+
op: "open";
|
|
10
|
+
shell?: string;
|
|
11
|
+
}> | Readonly<{
|
|
12
|
+
command: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
name: string;
|
|
15
|
+
op: "send";
|
|
16
|
+
shell?: string;
|
|
17
|
+
}> | Readonly<{
|
|
18
|
+
name: string;
|
|
19
|
+
op: "status";
|
|
20
|
+
}> | Readonly<{
|
|
21
|
+
op: "list";
|
|
22
|
+
}> | Readonly<{
|
|
23
|
+
name: string;
|
|
24
|
+
op: "close";
|
|
25
|
+
}> | Readonly<{
|
|
26
|
+
op: "ping";
|
|
27
|
+
}> | Readonly<{
|
|
28
|
+
op: "shutdown";
|
|
29
|
+
}>;
|
|
30
|
+
export type RunnethCliPublicSession = Readonly<{
|
|
31
|
+
busy: boolean;
|
|
32
|
+
cwd: string;
|
|
33
|
+
name: string;
|
|
34
|
+
pid: number;
|
|
35
|
+
running: boolean;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
shell: string;
|
|
38
|
+
startedAt: string;
|
|
39
|
+
}>;
|
|
40
|
+
export type RunnethCliResponse = Readonly<{
|
|
41
|
+
ok: true;
|
|
42
|
+
op: "open";
|
|
43
|
+
session: RunnethCliPublicSession;
|
|
44
|
+
}> | Readonly<{
|
|
45
|
+
exitCode: number;
|
|
46
|
+
ok: true;
|
|
47
|
+
op: "send";
|
|
48
|
+
session: RunnethCliPublicSession;
|
|
49
|
+
stderr: string;
|
|
50
|
+
stdout: string;
|
|
51
|
+
}> | Readonly<{
|
|
52
|
+
ok: true;
|
|
53
|
+
op: "status";
|
|
54
|
+
session: RunnethCliPublicSession | null;
|
|
55
|
+
}> | Readonly<{
|
|
56
|
+
ok: true;
|
|
57
|
+
op: "list";
|
|
58
|
+
sessions: readonly RunnethCliPublicSession[];
|
|
59
|
+
}> | Readonly<{
|
|
60
|
+
ok: true;
|
|
61
|
+
op: "close";
|
|
62
|
+
session: RunnethCliPublicSession;
|
|
63
|
+
}> | Readonly<{
|
|
64
|
+
ok: true;
|
|
65
|
+
op: "ping";
|
|
66
|
+
}> | Readonly<{
|
|
67
|
+
ok: true;
|
|
68
|
+
op: "shutdown";
|
|
69
|
+
}> | Readonly<{
|
|
70
|
+
error: string;
|
|
71
|
+
ok: false;
|
|
72
|
+
}>;
|
|
73
|
+
export type RunnethCliDaemonOptions = Readonly<{
|
|
74
|
+
socketPath: string;
|
|
75
|
+
}>;
|
|
76
|
+
type ExtractedCommandResult = Readonly<{
|
|
77
|
+
consumedStdoutBytes: number;
|
|
78
|
+
exitCode: number;
|
|
79
|
+
stdout: string;
|
|
80
|
+
}>;
|
|
81
|
+
export declare const normalizeSessionName: (value: string | undefined) => string;
|
|
82
|
+
export declare const parseRunnethCliRequest: (value: unknown) => RunnethCliRequest;
|
|
83
|
+
export declare const createCommandEnvelope: (command: string) => Readonly<{
|
|
84
|
+
input: string;
|
|
85
|
+
marker: string;
|
|
86
|
+
}>;
|
|
87
|
+
export declare const extractCommandResult: (input: {
|
|
88
|
+
readonly marker: string;
|
|
89
|
+
readonly stdout: Buffer;
|
|
90
|
+
}) => ExtractedCommandResult | null;
|
|
91
|
+
export declare class RunnethCliDaemon {
|
|
92
|
+
#private;
|
|
93
|
+
start(options: RunnethCliDaemonOptions): Promise<void>;
|
|
94
|
+
stop(): Promise<void>;
|
|
95
|
+
handleRequest(request: RunnethCliRequest): Promise<RunnethCliResponse>;
|
|
96
|
+
}
|
|
97
|
+
export declare const sendRunnethCliRequest: (input: {
|
|
98
|
+
readonly request: RunnethCliRequest;
|
|
99
|
+
readonly socketPath: string;
|
|
100
|
+
readonly timeoutMs?: number;
|
|
101
|
+
}) => Promise<RunnethCliResponse>;
|
|
102
|
+
export declare const startDaemonProcess: (input: {
|
|
103
|
+
readonly cliPath: string;
|
|
104
|
+
readonly socketPath: string;
|
|
105
|
+
}) => Promise<void>;
|
|
106
|
+
export declare const ensureDaemon: (input: {
|
|
107
|
+
readonly cliPath: string;
|
|
108
|
+
readonly socketPath: string;
|
|
109
|
+
}) => Promise<void>;
|