withruntime 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DAYTONA.md +129 -0
- package/E2B.md +2 -2
- package/README.md +47 -9
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +939 -102
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +26 -0
- package/dist/daytona/context.js +22 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/sandbox.d.ts +22 -2
- package/dist/e2b/sandbox.js +39 -7
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +14 -2
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/sandbox.d.ts +26 -1
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +31 -4
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +56 -4
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
/* Daytona's error classes by the same names and parents, so a
|
|
3
|
+
`catch (e) { if (e instanceof DaytonaNotFoundError) ... }` written for
|
|
4
|
+
Daytona keeps working. One made from a Runtime answer carries Runtime's
|
|
5
|
+
`code`, `hint` and `requestId`, and the original error as `cause`. */
|
|
6
|
+
export const SOURCE_API = "DAYTONA_API";
|
|
7
|
+
export const SOURCE_DAEMON = "DAYTONA_DAEMON";
|
|
8
|
+
export const SOURCE_PROXY = "DAYTONA_PROXY";
|
|
9
|
+
export class DaytonaError extends Error {
|
|
10
|
+
statusCode;
|
|
11
|
+
code;
|
|
12
|
+
source;
|
|
13
|
+
headers;
|
|
14
|
+
hint;
|
|
15
|
+
requestId;
|
|
16
|
+
constructor(message, statusCode, headers, code, source) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = new.target.name;
|
|
19
|
+
if (statusCode !== undefined)
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
if (headers !== undefined)
|
|
22
|
+
this.headers = headers;
|
|
23
|
+
if (code !== undefined)
|
|
24
|
+
this.code = code;
|
|
25
|
+
if (source !== undefined)
|
|
26
|
+
this.source = source;
|
|
27
|
+
}
|
|
28
|
+
/** Daytona's older name for `code`. */
|
|
29
|
+
get errorCode() {
|
|
30
|
+
return this.code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class DaytonaBadRequestError extends DaytonaError {
|
|
34
|
+
}
|
|
35
|
+
export class DaytonaAuthenticationError extends DaytonaError {
|
|
36
|
+
}
|
|
37
|
+
export class DaytonaForbiddenError extends DaytonaError {
|
|
38
|
+
}
|
|
39
|
+
export class DaytonaNotFoundError extends DaytonaError {
|
|
40
|
+
}
|
|
41
|
+
export class DaytonaTimeoutError extends DaytonaError {
|
|
42
|
+
}
|
|
43
|
+
export class DaytonaConflictError extends DaytonaError {
|
|
44
|
+
}
|
|
45
|
+
export class DaytonaGoneError extends DaytonaError {
|
|
46
|
+
}
|
|
47
|
+
export class DaytonaUnprocessableEntityError extends DaytonaError {
|
|
48
|
+
}
|
|
49
|
+
export class DaytonaRateLimitError extends DaytonaError {
|
|
50
|
+
}
|
|
51
|
+
export class DaytonaInternalServerError extends DaytonaError {
|
|
52
|
+
}
|
|
53
|
+
export class DaytonaBadGatewayError extends DaytonaError {
|
|
54
|
+
}
|
|
55
|
+
export class DaytonaServiceUnavailableError extends DaytonaError {
|
|
56
|
+
}
|
|
57
|
+
export class DaytonaValidationError extends DaytonaBadRequestError {
|
|
58
|
+
}
|
|
59
|
+
export class DaytonaAuthorizationError extends DaytonaForbiddenError {
|
|
60
|
+
}
|
|
61
|
+
export class DaytonaInvalidArgumentError extends DaytonaValidationError {
|
|
62
|
+
}
|
|
63
|
+
export class DaytonaConnectionError extends DaytonaError {
|
|
64
|
+
}
|
|
65
|
+
export class DaytonaConnectionTimeoutError extends DaytonaConnectionError {
|
|
66
|
+
}
|
|
67
|
+
export class DaytonaGitAuthFailedError extends DaytonaAuthenticationError {
|
|
68
|
+
}
|
|
69
|
+
export class DaytonaGitRepoNotFoundError extends DaytonaNotFoundError {
|
|
70
|
+
}
|
|
71
|
+
export class DaytonaGitBranchNotFoundError extends DaytonaNotFoundError {
|
|
72
|
+
}
|
|
73
|
+
export class DaytonaGitBranchExistsError extends DaytonaConflictError {
|
|
74
|
+
}
|
|
75
|
+
export class DaytonaGitPushRejectedError extends DaytonaConflictError {
|
|
76
|
+
}
|
|
77
|
+
export class DaytonaGitDirtyWorktreeError extends DaytonaConflictError {
|
|
78
|
+
}
|
|
79
|
+
export class DaytonaGitMergeConflictError extends DaytonaConflictError {
|
|
80
|
+
}
|
|
81
|
+
export class DaytonaGitTransportFailedError extends DaytonaBadGatewayError {
|
|
82
|
+
}
|
|
83
|
+
export class DaytonaGitRemoteRejectedError extends DaytonaUnprocessableEntityError {
|
|
84
|
+
}
|
|
85
|
+
export class DaytonaFileNotFoundError extends DaytonaNotFoundError {
|
|
86
|
+
}
|
|
87
|
+
export class DaytonaFileAccessDeniedError extends DaytonaForbiddenError {
|
|
88
|
+
}
|
|
89
|
+
export class DaytonaInvalidFilePathError extends DaytonaBadRequestError {
|
|
90
|
+
}
|
|
91
|
+
export class DaytonaFileReadFailedError extends DaytonaInternalServerError {
|
|
92
|
+
}
|
|
93
|
+
export class DaytonaLspServerNotInitializedError extends DaytonaBadRequestError {
|
|
94
|
+
}
|
|
95
|
+
export class DaytonaProcessExecutionTimeoutError extends DaytonaTimeoutError {
|
|
96
|
+
}
|
|
97
|
+
export class DaytonaProcessNotFoundError extends DaytonaNotFoundError {
|
|
98
|
+
}
|
|
99
|
+
export class DaytonaSessionEndedError extends DaytonaGoneError {
|
|
100
|
+
}
|
|
101
|
+
export class DaytonaCommandAlreadyCompletedError extends DaytonaGoneError {
|
|
102
|
+
}
|
|
103
|
+
export class DaytonaA11yUnavailableError extends DaytonaServiceUnavailableError {
|
|
104
|
+
}
|
|
105
|
+
export class DaytonaRecordingStillActiveError extends DaytonaConflictError {
|
|
106
|
+
}
|
|
107
|
+
export class DaytonaRecordingFfmpegNotFoundError extends DaytonaServiceUnavailableError {
|
|
108
|
+
}
|
|
109
|
+
/** A call that Daytona supports and Runtime does not, or not in the same
|
|
110
|
+
* way. Thrown before anything is done, never after doing something
|
|
111
|
+
* different. `feature` names what was asked for; `alternative` says what to
|
|
112
|
+
* use on Runtime instead. */
|
|
113
|
+
export class NotSupportedError extends DaytonaError {
|
|
114
|
+
feature;
|
|
115
|
+
alternative;
|
|
116
|
+
constructor(feature, alternative, message) {
|
|
117
|
+
super(message ?? `${feature} is not supported on Runtime. ${alternative}`, undefined, undefined, "not_supported");
|
|
118
|
+
this.feature = feature;
|
|
119
|
+
this.alternative = alternative;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const BY_STATUS = {
|
|
123
|
+
400: DaytonaBadRequestError,
|
|
124
|
+
401: DaytonaAuthenticationError,
|
|
125
|
+
403: DaytonaForbiddenError,
|
|
126
|
+
404: DaytonaNotFoundError,
|
|
127
|
+
408: DaytonaTimeoutError,
|
|
128
|
+
409: DaytonaConflictError,
|
|
129
|
+
410: DaytonaGoneError,
|
|
130
|
+
413: DaytonaBadRequestError,
|
|
131
|
+
422: DaytonaUnprocessableEntityError,
|
|
132
|
+
429: DaytonaRateLimitError,
|
|
133
|
+
500: DaytonaInternalServerError,
|
|
134
|
+
502: DaytonaBadGatewayError,
|
|
135
|
+
503: DaytonaServiceUnavailableError,
|
|
136
|
+
504: DaytonaTimeoutError,
|
|
137
|
+
};
|
|
138
|
+
/** Turns a Runtime SDK error into the Daytona error a caller written for
|
|
139
|
+
* Daytona expects. Anything that is not a Runtime error passes through. */
|
|
140
|
+
export function translate(error, subject = "other") {
|
|
141
|
+
if (!(error instanceof RuntimeError))
|
|
142
|
+
return error;
|
|
143
|
+
const message = [
|
|
144
|
+
error.message,
|
|
145
|
+
error.hint ? `Hint: ${error.hint}` : "",
|
|
146
|
+
error.requestId ? `Request: ${error.requestId}` : "",
|
|
147
|
+
]
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join("\n");
|
|
150
|
+
let out;
|
|
151
|
+
if (error.status === 503 && error.code.endsWith("_unavailable"))
|
|
152
|
+
out = new NotSupportedError(error.code.replace(/_unavailable$/, ""), error.hint ?? "", message);
|
|
153
|
+
else if (error.code === "file_not_found" || (error.status === 404 && subject === "file"))
|
|
154
|
+
out = new DaytonaFileNotFoundError(message);
|
|
155
|
+
else if (error.status === 404 && subject === "process")
|
|
156
|
+
out = new DaytonaProcessNotFoundError(message);
|
|
157
|
+
else if (error.code === "command_timeout")
|
|
158
|
+
out = new DaytonaProcessExecutionTimeoutError(message);
|
|
159
|
+
else if (error.status === 0)
|
|
160
|
+
out = new DaytonaConnectionError(message);
|
|
161
|
+
else
|
|
162
|
+
out = new (BY_STATUS[error.status] ?? DaytonaError)(message);
|
|
163
|
+
if (error.status)
|
|
164
|
+
out.statusCode = error.status;
|
|
165
|
+
if (!(out instanceof NotSupportedError))
|
|
166
|
+
out.code = error.code;
|
|
167
|
+
if (error.hint)
|
|
168
|
+
out.hint = error.hint;
|
|
169
|
+
if (error.requestId)
|
|
170
|
+
out.requestId = error.requestId;
|
|
171
|
+
out.cause = error;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** Runs `work`, turning any Runtime error into its Daytona counterpart. */
|
|
175
|
+
export async function guard(subject, work) {
|
|
176
|
+
try {
|
|
177
|
+
return await work();
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
throw translate(error, subject);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import { type SandboxContext } from "./context.js";
|
|
3
|
+
export interface FileInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
isDir: boolean;
|
|
6
|
+
size: number;
|
|
7
|
+
modTime: string;
|
|
8
|
+
mode: string;
|
|
9
|
+
permissions: string;
|
|
10
|
+
/** Runtime's listing does not say who owns a file: always "". */
|
|
11
|
+
owner: string;
|
|
12
|
+
group: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Match {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ReplaceResult {
|
|
20
|
+
file?: string;
|
|
21
|
+
success?: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SearchFilesResponse {
|
|
25
|
+
files: string[];
|
|
26
|
+
}
|
|
27
|
+
export type FilePermissionsParams = {
|
|
28
|
+
group?: string;
|
|
29
|
+
mode?: string;
|
|
30
|
+
owner?: string;
|
|
31
|
+
};
|
|
32
|
+
export interface FileUpload {
|
|
33
|
+
source: string | Buffer | Uint8Array;
|
|
34
|
+
destination: string;
|
|
35
|
+
}
|
|
36
|
+
export interface FileDownloadRequest {
|
|
37
|
+
source: string;
|
|
38
|
+
destination?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface FileDownloadResponse {
|
|
41
|
+
source: string;
|
|
42
|
+
result?: Buffer | string;
|
|
43
|
+
error?: string;
|
|
44
|
+
errorDetails?: {
|
|
45
|
+
message: string;
|
|
46
|
+
statusCode?: number;
|
|
47
|
+
code?: string;
|
|
48
|
+
source?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export type UploadSource = Buffer | Uint8Array | string | Readable | ReadableStream<Uint8Array>;
|
|
52
|
+
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
53
|
+
* paths resolve from the working directory, /workspace on Runtime. */
|
|
54
|
+
export declare class FileSystem {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(ctx: SandboxContext);
|
|
57
|
+
/** Makes a directory and its parents, with `mode` (such as "755"). */
|
|
58
|
+
createFolder(path: string, mode: string): Promise<void>;
|
|
59
|
+
deleteFile(path: string, recursive?: boolean): Promise<void>;
|
|
60
|
+
/** The file's bytes, or with a local path, the file written there. */
|
|
61
|
+
downloadFile(remotePath: string, timeout?: number): Promise<Buffer>;
|
|
62
|
+
downloadFile(remotePath: string, localPath: string, timeout?: number): Promise<void>;
|
|
63
|
+
downloadFileStream(remotePath: string): Promise<Readable>;
|
|
64
|
+
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
65
|
+
downloadFiles(files: FileDownloadRequest[]): Promise<FileDownloadResponse[]>;
|
|
66
|
+
/** Lines matching `pattern` (a grep pattern) in files under `path`. */
|
|
67
|
+
findFiles(path: string, pattern: string): Promise<Match[]>;
|
|
68
|
+
getFileDetails(path: string): Promise<FileInfo>;
|
|
69
|
+
listFiles(path: string, options?: {
|
|
70
|
+
depth?: number;
|
|
71
|
+
}): Promise<FileInfo[]>;
|
|
72
|
+
moveFiles(source: string, destination: string): Promise<void>;
|
|
73
|
+
/** Replaces every occurrence of `pattern` (plain text) in each file. */
|
|
74
|
+
replaceInFiles(files: string[], pattern: string, newValue: string): Promise<ReplaceResult[]>;
|
|
75
|
+
/** Paths under `path` matching a glob such as `*.py`. */
|
|
76
|
+
searchFiles(path: string, pattern: string): Promise<SearchFilesResponse>;
|
|
77
|
+
setFilePermissions(path: string, permissions: FilePermissionsParams): Promise<void>;
|
|
78
|
+
/** Writes a file (from bytes, or from a local path), making its
|
|
79
|
+
* directories and replacing one that exists. */
|
|
80
|
+
uploadFile(file: Buffer, remotePath: string, timeout?: number): Promise<void>;
|
|
81
|
+
uploadFile(localPath: string, remotePath: string, timeout?: number): Promise<void>;
|
|
82
|
+
uploadFileStream(source: UploadSource, remotePath: string): Promise<void>;
|
|
83
|
+
uploadFiles(files: FileUpload[]): Promise<void>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { resolvePath } from "./context.js";
|
|
2
|
+
import { DaytonaError, DaytonaFileNotFoundError, guard } from "./errors.js";
|
|
3
|
+
const PERMISSIONS = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
|
|
4
|
+
function fileInfo(entry) {
|
|
5
|
+
const bits = (parseInt(entry.mode, 8) || 0) & 0o777;
|
|
6
|
+
const kind = entry.type === "directory" ? "d" : entry.type === "symlink" ? "l" : "-";
|
|
7
|
+
return {
|
|
8
|
+
name: entry.name,
|
|
9
|
+
isDir: entry.type === "directory",
|
|
10
|
+
size: entry.size,
|
|
11
|
+
modTime: entry.modifiedAt,
|
|
12
|
+
mode: `${kind}${PERMISSIONS[(bits >> 6) & 7]}${PERMISSIONS[(bits >> 3) & 7]}${PERMISSIONS[bits & 7]}`,
|
|
13
|
+
permissions: `0${bits.toString(8).padStart(3, "0")}`,
|
|
14
|
+
owner: "",
|
|
15
|
+
group: "",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function bytesOf(source) {
|
|
19
|
+
if (typeof source === "string") {
|
|
20
|
+
const { readFile } = await import("node:fs/promises");
|
|
21
|
+
return readFile(source);
|
|
22
|
+
}
|
|
23
|
+
if (source instanceof Uint8Array)
|
|
24
|
+
return source;
|
|
25
|
+
if (typeof source.getReader === "function")
|
|
26
|
+
return new Uint8Array(await new Response(source).arrayBuffer());
|
|
27
|
+
const chunks = [];
|
|
28
|
+
for await (const chunk of source)
|
|
29
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
30
|
+
return Buffer.concat(chunks);
|
|
31
|
+
}
|
|
32
|
+
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
33
|
+
* paths resolve from the working directory, /workspace on Runtime. */
|
|
34
|
+
export class FileSystem {
|
|
35
|
+
#ctx;
|
|
36
|
+
constructor(ctx) {
|
|
37
|
+
this.#ctx = ctx;
|
|
38
|
+
}
|
|
39
|
+
async #path(path) {
|
|
40
|
+
await this.#ctx.ensureHome(path);
|
|
41
|
+
return resolvePath(path);
|
|
42
|
+
}
|
|
43
|
+
async #files() {
|
|
44
|
+
return (await this.#ctx.live()).files;
|
|
45
|
+
}
|
|
46
|
+
async #run(argv) {
|
|
47
|
+
const runtime = await this.#ctx.live();
|
|
48
|
+
const result = await guard("sandbox", () => runtime.exec(argv));
|
|
49
|
+
if (result.exitCode !== 0) {
|
|
50
|
+
const message = result.stderr.trim() || `${argv[0]} exited with ${result.exitCode}`;
|
|
51
|
+
if (/No such file or directory/.test(message))
|
|
52
|
+
throw new DaytonaFileNotFoundError(message, 404);
|
|
53
|
+
throw new DaytonaError(message, 400);
|
|
54
|
+
}
|
|
55
|
+
return result.stdout;
|
|
56
|
+
}
|
|
57
|
+
/** Makes a directory and its parents, with `mode` (such as "755"). */
|
|
58
|
+
async createFolder(path, mode) {
|
|
59
|
+
const target = await this.#path(path);
|
|
60
|
+
await this.#run(["mkdir", "-p", "-m", mode, "--", target]);
|
|
61
|
+
}
|
|
62
|
+
async deleteFile(path, recursive = false) {
|
|
63
|
+
const target = await this.#path(path);
|
|
64
|
+
const files = await this.#files();
|
|
65
|
+
const removed = await guard("file", () => files.remove(target, { recursive }));
|
|
66
|
+
if (!removed)
|
|
67
|
+
throw new DaytonaFileNotFoundError(`${target} does not exist.`, 404);
|
|
68
|
+
}
|
|
69
|
+
async downloadFile(remotePath, localOrTimeout) {
|
|
70
|
+
const target = await this.#path(remotePath);
|
|
71
|
+
const files = await this.#files();
|
|
72
|
+
const bytes = Buffer.from(await guard("file", () => files.read(target)));
|
|
73
|
+
if (typeof localOrTimeout !== "string")
|
|
74
|
+
return bytes;
|
|
75
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
76
|
+
const paths = await import("node:path");
|
|
77
|
+
await mkdir(paths.dirname(localOrTimeout), { recursive: true });
|
|
78
|
+
await writeFile(localOrTimeout, bytes);
|
|
79
|
+
}
|
|
80
|
+
async downloadFileStream(remotePath) {
|
|
81
|
+
const { Readable } = await import("node:stream");
|
|
82
|
+
return Readable.from([await this.downloadFile(remotePath)]);
|
|
83
|
+
}
|
|
84
|
+
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
85
|
+
async downloadFiles(files) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
try {
|
|
89
|
+
if (file.destination) {
|
|
90
|
+
await this.downloadFile(file.source, file.destination);
|
|
91
|
+
out.push({ source: file.source, result: file.destination });
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
out.push({ source: file.source, result: await this.downloadFile(file.source) });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const failure = error;
|
|
98
|
+
out.push({
|
|
99
|
+
source: file.source,
|
|
100
|
+
error: failure.message,
|
|
101
|
+
errorDetails: {
|
|
102
|
+
message: failure.message,
|
|
103
|
+
...(failure.statusCode ? { statusCode: failure.statusCode } : {}),
|
|
104
|
+
...(failure.code ? { code: failure.code } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** Lines matching `pattern` (a grep pattern) in files under `path`. */
|
|
112
|
+
async findFiles(path, pattern) {
|
|
113
|
+
const target = await this.#path(path);
|
|
114
|
+
const runtime = await this.#ctx.live();
|
|
115
|
+
const result = await guard("sandbox", () => runtime.exec(["grep", "-rnI", "--", pattern, target]));
|
|
116
|
+
if (result.exitCode !== 0 && result.exitCode !== 1)
|
|
117
|
+
throw new DaytonaError(result.stderr.trim() || "grep failed", 400);
|
|
118
|
+
return result.stdout
|
|
119
|
+
.split("\n")
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map((line) => {
|
|
122
|
+
const match = /^(.*?):(\d+):(.*)$/.exec(line);
|
|
123
|
+
return match
|
|
124
|
+
? { file: match[1], line: Number(match[2]), content: match[3] }
|
|
125
|
+
: { file: line, line: 0, content: "" };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async getFileDetails(path) {
|
|
129
|
+
const target = await this.#path(path);
|
|
130
|
+
const files = await this.#files();
|
|
131
|
+
const found = await guard("file", () => files.stat(target));
|
|
132
|
+
if (!found.exists)
|
|
133
|
+
throw new DaytonaFileNotFoundError(`${target} does not exist.`, 404);
|
|
134
|
+
return fileInfo(found);
|
|
135
|
+
}
|
|
136
|
+
async listFiles(path, options = {}) {
|
|
137
|
+
const target = await this.#path(path);
|
|
138
|
+
const files = await this.#files();
|
|
139
|
+
const entries = await guard("file", () => files.list(target, { depth: options.depth ?? 1, hidden: true }));
|
|
140
|
+
return entries.map(fileInfo);
|
|
141
|
+
}
|
|
142
|
+
async moveFiles(source, destination) {
|
|
143
|
+
const from = await this.#path(source);
|
|
144
|
+
const to = await this.#path(destination);
|
|
145
|
+
const files = await this.#files();
|
|
146
|
+
await guard("file", () => files.rename(from, to, { overwrite: true }));
|
|
147
|
+
}
|
|
148
|
+
/** Replaces every occurrence of `pattern` (plain text) in each file. */
|
|
149
|
+
async replaceInFiles(files, pattern, newValue) {
|
|
150
|
+
const api = await this.#files();
|
|
151
|
+
const results = [];
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
const target = await this.#path(file);
|
|
154
|
+
try {
|
|
155
|
+
const text = new TextDecoder().decode(await guard("file", () => api.read(target)));
|
|
156
|
+
await guard("file", () => api.write(target, text.split(pattern).join(newValue)));
|
|
157
|
+
results.push({ file, success: true });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
results.push({ file, success: false, error: error.message });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
/** Paths under `path` matching a glob such as `*.py`. */
|
|
166
|
+
async searchFiles(path, pattern) {
|
|
167
|
+
const target = await this.#path(path);
|
|
168
|
+
const files = await this.#files();
|
|
169
|
+
const glob = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
|
|
170
|
+
const entries = await guard("file", () => files.list(target, { glob, hidden: true }));
|
|
171
|
+
return { files: entries.map((entry) => entry.path) };
|
|
172
|
+
}
|
|
173
|
+
async setFilePermissions(path, permissions) {
|
|
174
|
+
const target = await this.#path(path);
|
|
175
|
+
if (permissions.mode)
|
|
176
|
+
await this.#run(["chmod", permissions.mode, "--", target]);
|
|
177
|
+
if (permissions.owner || permissions.group)
|
|
178
|
+
await this.#run([
|
|
179
|
+
"sudo",
|
|
180
|
+
"chown",
|
|
181
|
+
`${permissions.owner ?? ""}${permissions.group ? `:${permissions.group}` : ""}`,
|
|
182
|
+
"--",
|
|
183
|
+
target,
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
async uploadFile(source, remotePath) {
|
|
187
|
+
const target = await this.#path(remotePath);
|
|
188
|
+
const bytes = await bytesOf(source);
|
|
189
|
+
const files = await this.#files();
|
|
190
|
+
await guard("file", () => files.write(target, bytes));
|
|
191
|
+
}
|
|
192
|
+
async uploadFileStream(source, remotePath) {
|
|
193
|
+
const target = await this.#path(remotePath);
|
|
194
|
+
const bytes = await bytesOf(source);
|
|
195
|
+
const files = await this.#files();
|
|
196
|
+
await guard("file", () => files.write(target, bytes));
|
|
197
|
+
}
|
|
198
|
+
async uploadFiles(files) {
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
const target = await this.#path(file.destination);
|
|
201
|
+
const bytes = await bytesOf(file.source);
|
|
202
|
+
const api = await this.#files();
|
|
203
|
+
await guard("file", () => api.write(target, bytes));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SandboxContext } from "./context.js";
|
|
2
|
+
export interface GitStatus {
|
|
3
|
+
currentBranch: string;
|
|
4
|
+
ahead?: number;
|
|
5
|
+
behind?: number;
|
|
6
|
+
branchPublished?: boolean;
|
|
7
|
+
fileStatus: {
|
|
8
|
+
name: string;
|
|
9
|
+
staging: string;
|
|
10
|
+
worktree: string;
|
|
11
|
+
extra: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
export interface ListBranchResponse {
|
|
15
|
+
branches: string[];
|
|
16
|
+
}
|
|
17
|
+
/** `sandbox.git`: Daytona's git calls, run with the git in the sandbox. */
|
|
18
|
+
export declare class Git {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(ctx: SandboxContext);
|
|
21
|
+
clone(url: string, path: string, branch?: string, commitId?: string, username?: string, password?: string): Promise<void>;
|
|
22
|
+
add(path: string, files: string[]): Promise<void>;
|
|
23
|
+
branches(path: string): Promise<ListBranchResponse>;
|
|
24
|
+
createBranch(path: string, name: string): Promise<void>;
|
|
25
|
+
checkoutBranch(path: string, branch: string): Promise<void>;
|
|
26
|
+
deleteBranch(path: string, name: string): Promise<void>;
|
|
27
|
+
commit(path: string, message: string, author: string, email: string, allowEmpty?: boolean): Promise<{
|
|
28
|
+
sha: string;
|
|
29
|
+
}>;
|
|
30
|
+
push(path: string, username?: string, password?: string): Promise<void>;
|
|
31
|
+
pull(path: string, username?: string, password?: string): Promise<void>;
|
|
32
|
+
status(path: string): Promise<GitStatus>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { resolvePath } from "./context.js";
|
|
2
|
+
import { DaytonaError, DaytonaGitAuthFailedError, DaytonaGitBranchExistsError, DaytonaGitBranchNotFoundError, DaytonaGitMergeConflictError, DaytonaGitPushRejectedError, DaytonaGitRepoNotFoundError, guard, } from "./errors.js";
|
|
3
|
+
/** Credentials reach git through a helper reading the environment, never the
|
|
4
|
+
* command line or the remote URL. */
|
|
5
|
+
const HELPER = 'credential.helper=!f() { echo "username=$GIT_USER"; echo "password=$GIT_PASS"; }; f';
|
|
6
|
+
function failure(stderr, fallback) {
|
|
7
|
+
const message = stderr.trim() || fallback;
|
|
8
|
+
if (/Authentication failed|could not read Username|403/.test(message))
|
|
9
|
+
return new DaytonaGitAuthFailedError(message, 401);
|
|
10
|
+
if (/Repository not found|not found|does not appear to be a git repository/i.test(message))
|
|
11
|
+
return new DaytonaGitRepoNotFoundError(message, 404);
|
|
12
|
+
if (/already exists/.test(message))
|
|
13
|
+
return new DaytonaGitBranchExistsError(message, 409);
|
|
14
|
+
if (/did not match any|not a valid|unknown revision|branch .* not found/i.test(message))
|
|
15
|
+
return new DaytonaGitBranchNotFoundError(message, 404);
|
|
16
|
+
if (/rejected|non-fast-forward/.test(message))
|
|
17
|
+
return new DaytonaGitPushRejectedError(message, 409);
|
|
18
|
+
if (/CONFLICT|Merge conflict/.test(message))
|
|
19
|
+
return new DaytonaGitMergeConflictError(message, 409);
|
|
20
|
+
return new DaytonaError(message, 400);
|
|
21
|
+
}
|
|
22
|
+
/** `sandbox.git`: Daytona's git calls, run with the git in the sandbox. */
|
|
23
|
+
export class Git {
|
|
24
|
+
#ctx;
|
|
25
|
+
constructor(ctx) {
|
|
26
|
+
this.#ctx = ctx;
|
|
27
|
+
}
|
|
28
|
+
async #git(args, auth) {
|
|
29
|
+
const runtime = await this.#ctx.live();
|
|
30
|
+
const withAuth = auth?.username !== undefined || auth?.password !== undefined;
|
|
31
|
+
const result = await guard("sandbox", () => runtime.exec(["git", ...(withAuth ? ["-c", HELPER] : []), ...args], {
|
|
32
|
+
timeoutMs: 600_000,
|
|
33
|
+
env: {
|
|
34
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
35
|
+
...(withAuth ? { GIT_USER: auth.username ?? "git", GIT_PASS: auth.password ?? "" } : {}),
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
if (result.exitCode !== 0)
|
|
39
|
+
throw failure(result.stderr, `git ${args[0]} failed`);
|
|
40
|
+
return result.stdout;
|
|
41
|
+
}
|
|
42
|
+
async #repo(path) {
|
|
43
|
+
await this.#ctx.ensureHome(path);
|
|
44
|
+
return resolvePath(path);
|
|
45
|
+
}
|
|
46
|
+
async clone(url, path, branch, commitId, username, password) {
|
|
47
|
+
const target = await this.#repo(path);
|
|
48
|
+
await this.#git(["clone", ...(branch ? ["--branch", branch] : []), "--", url, target], {
|
|
49
|
+
...(username !== undefined ? { username } : {}),
|
|
50
|
+
...(password !== undefined ? { password } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (commitId)
|
|
53
|
+
await this.#git(["-C", target, "checkout", commitId]);
|
|
54
|
+
}
|
|
55
|
+
async add(path, files) {
|
|
56
|
+
await this.#git(["-C", await this.#repo(path), "add", "--", ...files]);
|
|
57
|
+
}
|
|
58
|
+
async branches(path) {
|
|
59
|
+
const out = await this.#git([
|
|
60
|
+
"-C",
|
|
61
|
+
await this.#repo(path),
|
|
62
|
+
"branch",
|
|
63
|
+
"--format=%(refname:short)",
|
|
64
|
+
]);
|
|
65
|
+
return { branches: out.split("\n").filter(Boolean) };
|
|
66
|
+
}
|
|
67
|
+
async createBranch(path, name) {
|
|
68
|
+
await this.#git(["-C", await this.#repo(path), "switch", "-c", name]);
|
|
69
|
+
}
|
|
70
|
+
async checkoutBranch(path, branch) {
|
|
71
|
+
await this.#git(["-C", await this.#repo(path), "checkout", branch]);
|
|
72
|
+
}
|
|
73
|
+
async deleteBranch(path, name) {
|
|
74
|
+
await this.#git(["-C", await this.#repo(path), "branch", "-D", name]);
|
|
75
|
+
}
|
|
76
|
+
async commit(path, message, author, email, allowEmpty = false) {
|
|
77
|
+
const repo = await this.#repo(path);
|
|
78
|
+
await this.#git([
|
|
79
|
+
"-C",
|
|
80
|
+
repo,
|
|
81
|
+
"-c",
|
|
82
|
+
`user.name=${author}`,
|
|
83
|
+
"-c",
|
|
84
|
+
`user.email=${email}`,
|
|
85
|
+
"commit",
|
|
86
|
+
"-m",
|
|
87
|
+
message,
|
|
88
|
+
...(allowEmpty ? ["--allow-empty"] : []),
|
|
89
|
+
]);
|
|
90
|
+
return { sha: (await this.#git(["-C", repo, "rev-parse", "HEAD"])).trim() };
|
|
91
|
+
}
|
|
92
|
+
async push(path, username, password) {
|
|
93
|
+
await this.#git(["-C", await this.#repo(path), "push"], {
|
|
94
|
+
...(username !== undefined ? { username } : {}),
|
|
95
|
+
...(password !== undefined ? { password } : {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async pull(path, username, password) {
|
|
99
|
+
await this.#git(["-C", await this.#repo(path), "pull"], {
|
|
100
|
+
...(username !== undefined ? { username } : {}),
|
|
101
|
+
...(password !== undefined ? { password } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async status(path) {
|
|
105
|
+
const out = await this.#git([
|
|
106
|
+
"-C",
|
|
107
|
+
await this.#repo(path),
|
|
108
|
+
"status",
|
|
109
|
+
"--porcelain=v1",
|
|
110
|
+
"--branch",
|
|
111
|
+
]);
|
|
112
|
+
const [head = "", ...lines] = out.split("\n");
|
|
113
|
+
const branch = /^## (?:No commits yet on )?([^.\s]+)(?:\.\.\.(\S+))?(?: \[(.*)\])?/.exec(head);
|
|
114
|
+
const counts = branch?.[3] ?? "";
|
|
115
|
+
return {
|
|
116
|
+
currentBranch: branch?.[1] ?? "",
|
|
117
|
+
ahead: Number(/ahead (\d+)/.exec(counts)?.[1] ?? 0),
|
|
118
|
+
behind: Number(/behind (\d+)/.exec(counts)?.[1] ?? 0),
|
|
119
|
+
branchPublished: Boolean(branch?.[2]),
|
|
120
|
+
fileStatus: lines.filter(Boolean).map((line) => ({
|
|
121
|
+
name: line.slice(3),
|
|
122
|
+
staging: line[0] ?? " ",
|
|
123
|
+
worktree: line[1] ?? " ",
|
|
124
|
+
extra: "",
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A file of the build context, as Runtime's image builds take it. */
|
|
2
|
+
export type ContextFile = {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
encoding: "base64";
|
|
6
|
+
mode?: number;
|
|
7
|
+
};
|
|
8
|
+
declare const PYTHON: readonly ["3.9", "3.10", "3.11", "3.12", "3.13"];
|
|
9
|
+
/** Daytona's declarative image, kept as the Dockerfile it stands for and
|
|
10
|
+
* built as a Runtime image (`runtime.images.build`) when a sandbox or a
|
|
11
|
+
* snapshot is created from it. Local files go with the build as its context. */
|
|
12
|
+
export declare class Image {
|
|
13
|
+
#private;
|
|
14
|
+
private constructor();
|
|
15
|
+
/** The Dockerfile so far. */
|
|
16
|
+
get dockerfile(): string;
|
|
17
|
+
static base(image: string): Image;
|
|
18
|
+
static debianSlim(pythonVersion?: (typeof PYTHON)[number]): Image;
|
|
19
|
+
/** A local Dockerfile; files it copies come from its directory. */
|
|
20
|
+
static fromDockerfile(path: string): Image;
|
|
21
|
+
pipInstall(packages: string | string[], options?: {
|
|
22
|
+
indexUrl?: string;
|
|
23
|
+
extraIndexUrls?: string[];
|
|
24
|
+
findLinks?: string[];
|
|
25
|
+
pre?: boolean;
|
|
26
|
+
extraOptions?: string;
|
|
27
|
+
}): Image;
|
|
28
|
+
pipInstallFromRequirements(requirementsTxt: string): Image;
|
|
29
|
+
pipInstallFromPyproject(): Image;
|
|
30
|
+
addLocalFile(localPath: string, remotePath: string): Image;
|
|
31
|
+
addLocalDir(localPath: string, remotePath: string): Image;
|
|
32
|
+
runCommands(...commands: (string | string[])[]): Image;
|
|
33
|
+
env(envVars: Record<string, string>): Image;
|
|
34
|
+
workdir(dirPath: string): Image;
|
|
35
|
+
entrypoint(entrypointCommands: string[]): Image;
|
|
36
|
+
cmd(cmd: string[]): Image;
|
|
37
|
+
dockerfileCommands(dockerfileCommands: string[]): Image;
|
|
38
|
+
/** The Dockerfile and its context files, read from the local disk. */
|
|
39
|
+
build(): Promise<{
|
|
40
|
+
dockerfile: string;
|
|
41
|
+
files: ContextFile[];
|
|
42
|
+
name: string;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|