@watasu/sdk 0.1.25 → 0.1.40
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 +50 -7
- package/dist/codeInterpreter.d.ts +33 -7
- package/dist/codeInterpreter.js +51 -10
- package/dist/commands.d.ts +5 -6
- package/dist/commands.js +6 -6
- package/dist/connectionConfig.d.ts +24 -0
- package/dist/connectionConfig.js +49 -2
- package/dist/filesystem.d.ts +7 -19
- package/dist/filesystem.js +1 -5
- package/dist/git.d.ts +1 -2
- package/dist/git.js +7 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/process.d.ts +1 -1
- package/dist/process.js +3 -3
- package/dist/processSocket.d.ts +2 -1
- package/dist/processSocket.js +4 -2
- package/dist/pty.d.ts +3 -3
- package/dist/pty.js +9 -7
- package/dist/sandbox.d.ts +40 -27
- package/dist/sandbox.js +130 -49
- package/dist/template.d.ts +3 -2
- package/dist/template.js +13 -2
- package/dist/terminal.d.ts +2 -3
- package/dist/terminal.js +2 -3
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +23 -6
- package/dist/volume.d.ts +116 -0
- package/dist/volume.js +278 -0
- package/package.json +1 -1
package/dist/volume.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Blob } from 'node:buffer';
|
|
2
|
+
import { ConnectionConfig, type ConnectionOpts } from './connectionConfig.js';
|
|
3
|
+
import { ControlClient } from './transport.js';
|
|
4
|
+
export type VolumeFileType = 'file' | 'directory' | 'symlink' | string;
|
|
5
|
+
export type VolumeReadFormat = 'text' | 'bytes' | 'blob' | 'stream';
|
|
6
|
+
export type VolumeWriteData = string | Uint8Array | ArrayBuffer | Blob;
|
|
7
|
+
/** Control-plane metadata for a persistent Watasu volume. */
|
|
8
|
+
export interface VolumeInfo {
|
|
9
|
+
volumeId: string;
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
state?: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
sizeMb?: number;
|
|
15
|
+
sizeBytes?: number;
|
|
16
|
+
node?: string;
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
raw: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** File or directory metadata returned by volume content operations. */
|
|
23
|
+
export interface VolumeEntryStat {
|
|
24
|
+
path: string;
|
|
25
|
+
name: string;
|
|
26
|
+
type: VolumeFileType;
|
|
27
|
+
size?: number;
|
|
28
|
+
mode?: number;
|
|
29
|
+
uid?: number;
|
|
30
|
+
gid?: number;
|
|
31
|
+
atime?: unknown;
|
|
32
|
+
mtime?: unknown;
|
|
33
|
+
ctime?: unknown;
|
|
34
|
+
raw: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface VolumeApiParams extends ConnectionOpts {
|
|
37
|
+
team?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface VolumeConnectionConfig extends ConnectionOpts {
|
|
40
|
+
}
|
|
41
|
+
export interface VolumeListOpts extends ConnectionOpts {
|
|
42
|
+
team?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface VolumeListFilesOpts extends ConnectionOpts {
|
|
45
|
+
depth?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface VolumeReadFileOpts extends ConnectionOpts {
|
|
48
|
+
format?: VolumeReadFormat;
|
|
49
|
+
}
|
|
50
|
+
export interface VolumeWriteFileOpts extends ConnectionOpts {
|
|
51
|
+
uid?: number;
|
|
52
|
+
gid?: number;
|
|
53
|
+
mode?: number | string;
|
|
54
|
+
force?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface VolumeMetadataOpts extends ConnectionOpts {
|
|
57
|
+
uid?: number;
|
|
58
|
+
gid?: number;
|
|
59
|
+
mode?: number | string;
|
|
60
|
+
}
|
|
61
|
+
/** Persistent volume that can be mounted into sandboxes and edited while detached. */
|
|
62
|
+
export declare class Volume {
|
|
63
|
+
readonly volumeId: string;
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly token?: string;
|
|
67
|
+
private readonly config;
|
|
68
|
+
private readonly control;
|
|
69
|
+
constructor(opts: {
|
|
70
|
+
volumeId: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
token?: string;
|
|
73
|
+
connectionConfig: ConnectionConfig;
|
|
74
|
+
control?: ControlClient;
|
|
75
|
+
});
|
|
76
|
+
/** Create a persistent volume and return a connected SDK object. */
|
|
77
|
+
static create(name: string, opts?: VolumeApiParams): Promise<Volume>;
|
|
78
|
+
/** Connect to an existing volume by id or name. */
|
|
79
|
+
static connect(volumeId: string, opts?: VolumeConnectionConfig): Promise<Volume>;
|
|
80
|
+
/** Fetch metadata for an existing volume by id or name. */
|
|
81
|
+
static getInfo(volumeId: string, opts?: VolumeConnectionConfig): Promise<VolumeInfo>;
|
|
82
|
+
/** List volumes visible to the configured API key. */
|
|
83
|
+
static list(opts?: VolumeListOpts): Promise<VolumeInfo[]>;
|
|
84
|
+
/** Destroy a volume by id or name. Returns false when it does not exist. */
|
|
85
|
+
static destroy(volumeId: string, opts?: VolumeConnectionConfig): Promise<boolean>;
|
|
86
|
+
/** Fetch this volume's latest metadata. */
|
|
87
|
+
getInfo(): Promise<VolumeInfo>;
|
|
88
|
+
/** Fetch metadata for a path inside this volume. */
|
|
89
|
+
getInfo(path: string, opts?: ConnectionOpts): Promise<VolumeEntryStat>;
|
|
90
|
+
/** List files and directories under `path`. */
|
|
91
|
+
list(path?: string, opts?: VolumeListFilesOpts): Promise<VolumeEntryStat[]>;
|
|
92
|
+
/** Create a directory inside the detached volume. */
|
|
93
|
+
makeDir(path: string, opts?: VolumeWriteFileOpts): Promise<VolumeEntryStat>;
|
|
94
|
+
/** Return whether a path exists inside the detached volume. */
|
|
95
|
+
exists(path: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
96
|
+
/** Update ownership or mode metadata for a path. */
|
|
97
|
+
updateMetadata(path: string, opts?: VolumeMetadataOpts): Promise<VolumeEntryStat>;
|
|
98
|
+
/** Read a file from the detached volume. */
|
|
99
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
100
|
+
format: 'bytes';
|
|
101
|
+
}): Promise<Uint8Array>;
|
|
102
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
103
|
+
format: 'blob';
|
|
104
|
+
}): Promise<Blob>;
|
|
105
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
106
|
+
format: 'stream';
|
|
107
|
+
}): Promise<ReadableStream<Uint8Array>>;
|
|
108
|
+
readFile(path: string, opts?: VolumeReadFileOpts): Promise<string>;
|
|
109
|
+
/** Write a file into the detached volume. */
|
|
110
|
+
writeFile(path: string, data: VolumeWriteData, opts?: VolumeWriteFileOpts): Promise<VolumeEntryStat>;
|
|
111
|
+
/** Remove a file or directory from the detached volume. */
|
|
112
|
+
remove(path: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
113
|
+
/** Destroy this volume. Returns false when it no longer exists. */
|
|
114
|
+
destroy(opts?: ConnectionOpts): Promise<boolean>;
|
|
115
|
+
private configOptions;
|
|
116
|
+
}
|
package/dist/volume.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Blob } from 'node:buffer';
|
|
2
|
+
import { ConnectionConfig } from './connectionConfig.js';
|
|
3
|
+
import { NotFoundError, SandboxError } from './errors.js';
|
|
4
|
+
import { base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
|
5
|
+
import { ControlClient, withQuery } from './transport.js';
|
|
6
|
+
/** Persistent volume that can be mounted into sandboxes and edited while detached. */
|
|
7
|
+
export class Volume {
|
|
8
|
+
volumeId;
|
|
9
|
+
id;
|
|
10
|
+
name;
|
|
11
|
+
token;
|
|
12
|
+
config;
|
|
13
|
+
control;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.volumeId = String(opts.volumeId);
|
|
16
|
+
this.id = this.volumeId;
|
|
17
|
+
this.name = opts.name ?? this.volumeId;
|
|
18
|
+
this.token = opts.token;
|
|
19
|
+
this.config = opts.connectionConfig;
|
|
20
|
+
this.control = opts.control ?? new ControlClient(this.config);
|
|
21
|
+
}
|
|
22
|
+
/** Create a persistent volume and return a connected SDK object. */
|
|
23
|
+
static async create(name, opts = {}) {
|
|
24
|
+
const config = new ConnectionConfig(opts);
|
|
25
|
+
const control = new ControlClient(config);
|
|
26
|
+
const payload = await control.post('/volumes', {
|
|
27
|
+
json: compactRecord({ name, team: opts.team }),
|
|
28
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
29
|
+
signal: opts.signal,
|
|
30
|
+
});
|
|
31
|
+
return volumeFromPayload(payload, config, control);
|
|
32
|
+
}
|
|
33
|
+
/** Connect to an existing volume by id or name. */
|
|
34
|
+
static async connect(volumeId, opts = {}) {
|
|
35
|
+
const config = new ConnectionConfig(opts);
|
|
36
|
+
const control = new ControlClient(config);
|
|
37
|
+
const payload = await control.get(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
38
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
39
|
+
signal: opts.signal,
|
|
40
|
+
});
|
|
41
|
+
return volumeFromPayload(payload, config, control);
|
|
42
|
+
}
|
|
43
|
+
/** Fetch metadata for an existing volume by id or name. */
|
|
44
|
+
static async getInfo(volumeId, opts = {}) {
|
|
45
|
+
const config = new ConnectionConfig(opts);
|
|
46
|
+
const control = new ControlClient(config);
|
|
47
|
+
const payload = await control.get(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
48
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
49
|
+
signal: opts.signal,
|
|
50
|
+
});
|
|
51
|
+
return volumeInfo(record(payload.volume ?? payload));
|
|
52
|
+
}
|
|
53
|
+
/** List volumes visible to the configured API key. */
|
|
54
|
+
static async list(opts = {}) {
|
|
55
|
+
const config = new ConnectionConfig(opts);
|
|
56
|
+
const control = new ControlClient(config);
|
|
57
|
+
const path = withQuery('/volumes', { team: opts.team });
|
|
58
|
+
const payload = await control.get(path, {
|
|
59
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
60
|
+
signal: opts.signal,
|
|
61
|
+
});
|
|
62
|
+
const volumes = Array.isArray(payload.volumes) ? payload.volumes : [];
|
|
63
|
+
return volumes.map((item) => volumeInfo(record(item)));
|
|
64
|
+
}
|
|
65
|
+
/** Destroy a volume by id or name. Returns false when it does not exist. */
|
|
66
|
+
static async destroy(volumeId, opts = {}) {
|
|
67
|
+
const config = new ConnectionConfig(opts);
|
|
68
|
+
const control = new ControlClient(config);
|
|
69
|
+
try {
|
|
70
|
+
await control.delete(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
71
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
72
|
+
signal: opts.signal,
|
|
73
|
+
});
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof NotFoundError)
|
|
78
|
+
return false;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async getInfo(path, opts = {}) {
|
|
83
|
+
if (path === undefined) {
|
|
84
|
+
return Volume.getInfo(this.volumeId, this.configOptions(opts));
|
|
85
|
+
}
|
|
86
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/path`, { path }), {
|
|
87
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
88
|
+
signal: opts.signal,
|
|
89
|
+
});
|
|
90
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
91
|
+
}
|
|
92
|
+
/** List files and directories under `path`. */
|
|
93
|
+
async list(path = '/', opts = {}) {
|
|
94
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/directories`, {
|
|
95
|
+
path,
|
|
96
|
+
depth: opts.depth,
|
|
97
|
+
}), {
|
|
98
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
99
|
+
signal: opts.signal,
|
|
100
|
+
});
|
|
101
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
102
|
+
return entries.map((item) => volumeEntry(record(item)));
|
|
103
|
+
}
|
|
104
|
+
/** Create a directory inside the detached volume. */
|
|
105
|
+
async makeDir(path, opts = {}) {
|
|
106
|
+
const payload = await this.control.post(`/volumes/${this.volumeId}/directories`, {
|
|
107
|
+
json: compactRecord({ path, ...metadataPayload(opts) }),
|
|
108
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
109
|
+
signal: opts.signal,
|
|
110
|
+
});
|
|
111
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
112
|
+
}
|
|
113
|
+
/** Return whether a path exists inside the detached volume. */
|
|
114
|
+
async exists(path, opts = {}) {
|
|
115
|
+
try {
|
|
116
|
+
await this.getInfo(path, opts);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error instanceof NotFoundError)
|
|
121
|
+
return false;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Update ownership or mode metadata for a path. */
|
|
126
|
+
async updateMetadata(path, opts = {}) {
|
|
127
|
+
const payload = await this.control.patch(`/volumes/${this.volumeId}/path`, {
|
|
128
|
+
json: compactRecord({ path, ...metadataPayload(opts) }),
|
|
129
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
130
|
+
signal: opts.signal,
|
|
131
|
+
});
|
|
132
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
133
|
+
}
|
|
134
|
+
async readFile(path, opts = {}) {
|
|
135
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/files`, { path }), {
|
|
136
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
137
|
+
signal: opts.signal,
|
|
138
|
+
});
|
|
139
|
+
const file = record(payload.file ?? payload);
|
|
140
|
+
const content = file.content_b64 ?? file.contentBase64 ?? file.content ?? '';
|
|
141
|
+
switch (opts.format ?? 'text') {
|
|
142
|
+
case 'bytes':
|
|
143
|
+
return base64DecodeBytes(content);
|
|
144
|
+
case 'blob':
|
|
145
|
+
return new Blob([base64DecodeBytes(content)]);
|
|
146
|
+
case 'stream':
|
|
147
|
+
return new Blob([base64DecodeBytes(content)]).stream();
|
|
148
|
+
case 'text':
|
|
149
|
+
return file.content_b64 || file.contentBase64 ? base64DecodeText(content) : String(content);
|
|
150
|
+
default:
|
|
151
|
+
throw new SandboxError(`unsupported volume read format: ${String(opts.format)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Write a file into the detached volume. */
|
|
155
|
+
async writeFile(path, data, opts = {}) {
|
|
156
|
+
const bytes = await bytesFromWriteData(data);
|
|
157
|
+
const payload = await this.control.put(`/volumes/${this.volumeId}/files`, {
|
|
158
|
+
json: compactRecord({
|
|
159
|
+
path,
|
|
160
|
+
content_b64: base64Encode(bytes),
|
|
161
|
+
...metadataPayload(opts),
|
|
162
|
+
force: opts.force,
|
|
163
|
+
}),
|
|
164
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
165
|
+
signal: opts.signal,
|
|
166
|
+
});
|
|
167
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
168
|
+
}
|
|
169
|
+
/** Remove a file or directory from the detached volume. */
|
|
170
|
+
async remove(path, opts = {}) {
|
|
171
|
+
await this.control.delete(withQuery(`/volumes/${this.volumeId}/path`, { path }), {
|
|
172
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
173
|
+
signal: opts.signal,
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
/** Destroy this volume. Returns false when it no longer exists. */
|
|
178
|
+
async destroy(opts = {}) {
|
|
179
|
+
return Volume.destroy(this.volumeId, this.configOptions(opts));
|
|
180
|
+
}
|
|
181
|
+
configOptions(opts = {}) {
|
|
182
|
+
return {
|
|
183
|
+
apiKey: this.config.apiKey,
|
|
184
|
+
apiUrl: this.config.apiUrl,
|
|
185
|
+
dataPlaneDomain: this.config.dataPlaneDomain,
|
|
186
|
+
requestTimeoutMs: this.config.requestTimeoutMs,
|
|
187
|
+
headers: this.config.headers,
|
|
188
|
+
apiHeaders: this.config.apiHeaders,
|
|
189
|
+
debug: this.config.debug,
|
|
190
|
+
signal: this.config.signal,
|
|
191
|
+
proxy: this.config.proxy,
|
|
192
|
+
...opts,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function volumeFromPayload(payload, config, control) {
|
|
197
|
+
const info = volumeInfo(record(payload.volume ?? payload));
|
|
198
|
+
return new Volume({
|
|
199
|
+
volumeId: info.volumeId,
|
|
200
|
+
name: info.name,
|
|
201
|
+
token: info.token,
|
|
202
|
+
connectionConfig: config,
|
|
203
|
+
control,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function volumeInfo(payload) {
|
|
207
|
+
const id = payload.volume_id ?? payload.volumeId ?? payload.id;
|
|
208
|
+
if (id === undefined)
|
|
209
|
+
throw new SandboxError('volume response did not include id');
|
|
210
|
+
return {
|
|
211
|
+
volumeId: String(id),
|
|
212
|
+
id: String(id),
|
|
213
|
+
name: String(payload.name ?? id),
|
|
214
|
+
state: stringValue(payload.state),
|
|
215
|
+
token: stringValue(payload.token),
|
|
216
|
+
sizeMb: numberValue(payload.size_mb ?? payload.sizeMb),
|
|
217
|
+
sizeBytes: numberValue(payload.size_bytes ?? payload.sizeBytes),
|
|
218
|
+
node: stringValue(payload.node ?? payload.node_name ?? payload.nodeName),
|
|
219
|
+
metadata: recordOfStrings(payload.metadata),
|
|
220
|
+
createdAt: stringValue(payload.created_at ?? payload.createdAt),
|
|
221
|
+
updatedAt: stringValue(payload.updated_at ?? payload.updatedAt),
|
|
222
|
+
raw: payload,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function volumeEntry(payload) {
|
|
226
|
+
return {
|
|
227
|
+
path: String(payload.path ?? ''),
|
|
228
|
+
name: String(payload.name ?? ''),
|
|
229
|
+
type: String(payload.type ?? 'file'),
|
|
230
|
+
size: numberValue(payload.size ?? payload.bytes),
|
|
231
|
+
mode: numberValue(payload.mode),
|
|
232
|
+
uid: numberValue(payload.uid),
|
|
233
|
+
gid: numberValue(payload.gid),
|
|
234
|
+
atime: payload.atime,
|
|
235
|
+
mtime: payload.mtime,
|
|
236
|
+
ctime: payload.ctime,
|
|
237
|
+
raw: payload,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function bytesFromWriteData(data) {
|
|
241
|
+
if (typeof data === 'string')
|
|
242
|
+
return new TextEncoder().encode(data);
|
|
243
|
+
if (data instanceof Uint8Array)
|
|
244
|
+
return data;
|
|
245
|
+
if (data instanceof ArrayBuffer)
|
|
246
|
+
return new Uint8Array(data);
|
|
247
|
+
if (data instanceof Blob)
|
|
248
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
249
|
+
throw new SandboxError('unsupported volume write data');
|
|
250
|
+
}
|
|
251
|
+
function metadataPayload(opts) {
|
|
252
|
+
return compactRecord({
|
|
253
|
+
uid: opts.uid,
|
|
254
|
+
gid: opts.gid,
|
|
255
|
+
mode: opts.mode,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function compactRecord(payload) {
|
|
259
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
|
260
|
+
}
|
|
261
|
+
function record(value) {
|
|
262
|
+
return value && typeof value === 'object' ? value : {};
|
|
263
|
+
}
|
|
264
|
+
function recordOfStrings(value) {
|
|
265
|
+
if (!value || typeof value !== 'object')
|
|
266
|
+
return {};
|
|
267
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
|
|
268
|
+
}
|
|
269
|
+
function stringValue(value) {
|
|
270
|
+
if (typeof value === 'string')
|
|
271
|
+
return value;
|
|
272
|
+
if (typeof value === 'number')
|
|
273
|
+
return String(value);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
function numberValue(value) {
|
|
277
|
+
return typeof value === 'number' ? value : undefined;
|
|
278
|
+
}
|