@lizard-build/sdk 0.1.39 → 0.1.41
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 +58 -254
- package/dist/index.d.mts +559 -306
- package/dist/index.d.ts +559 -306
- package/dist/index.js +629 -285
- package/dist/index.mjs +625 -285
- package/package.json +9 -6
package/dist/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ var ConnectionConfig = class {
|
|
|
6
6
|
this.apiKey = opts?.apiKey ?? process.env.LIZARD_API_KEY ?? "";
|
|
7
7
|
this.apiUrl = opts?.apiUrl ?? process.env.LIZARD_API_URL ?? DEFAULT_API_URL;
|
|
8
8
|
this.timeoutMs = opts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
9
|
+
this.requestTimeoutMs = opts?.requestTimeoutMs ?? 12e4;
|
|
9
10
|
if (!this.apiKey) {
|
|
10
11
|
throw new Error(
|
|
11
12
|
"Lizard API key is required. Set LIZARD_API_KEY env var or pass apiKey in options."
|
|
@@ -27,6 +28,13 @@ var LizardError = class extends Error {
|
|
|
27
28
|
this.name = "LizardError";
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
var ConfigApplyError = class extends LizardError {
|
|
32
|
+
constructor(result) {
|
|
33
|
+
super("Config was saved, but one or more side effects failed; inspect result before retrying");
|
|
34
|
+
this.result = result;
|
|
35
|
+
this.name = "ConfigApplyError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
30
38
|
var AuthenticationError = class extends LizardError {
|
|
31
39
|
constructor(message = "Invalid or missing API key") {
|
|
32
40
|
super(message);
|
|
@@ -66,19 +74,358 @@ async function handleApiError(res) {
|
|
|
66
74
|
throw new LizardError(`API error ${res.status}: ${message}`);
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
// src/platform/client.ts
|
|
78
|
+
var segment = (value) => encodeURIComponent(value);
|
|
79
|
+
function query(path, values) {
|
|
80
|
+
const qs = new URLSearchParams();
|
|
81
|
+
for (const [key, value] of Object.entries(values)) if (value !== void 0) qs.set(key, String(value));
|
|
82
|
+
return qs.size ? `${path}?${qs}` : path;
|
|
83
|
+
}
|
|
84
|
+
var PlatformClient = class {
|
|
85
|
+
constructor(opts) {
|
|
86
|
+
this.config = new ConnectionConfig(opts);
|
|
87
|
+
}
|
|
88
|
+
async json(res) {
|
|
89
|
+
if (!res.ok) await handleApiError(res);
|
|
90
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
|
|
91
|
+
const text = await res.text();
|
|
92
|
+
return text ? JSON.parse(text) : void 0;
|
|
93
|
+
}
|
|
94
|
+
request(method, path, body) {
|
|
95
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
96
|
+
method,
|
|
97
|
+
headers: this.config.headers,
|
|
98
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
99
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
100
|
+
}).then((res) => this.json(res));
|
|
101
|
+
}
|
|
102
|
+
get(path) {
|
|
103
|
+
return this.request("GET", path);
|
|
104
|
+
}
|
|
105
|
+
post(path, body) {
|
|
106
|
+
return this.request("POST", path, body);
|
|
107
|
+
}
|
|
108
|
+
put(path, body) {
|
|
109
|
+
return this.request("PUT", path, body);
|
|
110
|
+
}
|
|
111
|
+
patch(path, body) {
|
|
112
|
+
return this.request("PATCH", path, body);
|
|
113
|
+
}
|
|
114
|
+
delete(path, body) {
|
|
115
|
+
return this.request("DELETE", path, body);
|
|
116
|
+
}
|
|
117
|
+
async applyConfig(projectId, body) {
|
|
118
|
+
const result = await this.post(`/api/projects/${projectId}/config:apply`, body);
|
|
119
|
+
if (result?.sideEffectFailures?.length) throw new ConfigApplyError(result);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
sendBytes(method, path, body, contentType = "application/octet-stream") {
|
|
123
|
+
const payload = typeof body === "string" || body instanceof Blob ? body : new Blob([new Uint8Array(body)]);
|
|
124
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
125
|
+
method,
|
|
126
|
+
headers: { ...this.config.headers, "Content-Type": contentType },
|
|
127
|
+
body: payload,
|
|
128
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
129
|
+
}).then((res) => this.json(res));
|
|
130
|
+
}
|
|
131
|
+
postForm(path, form) {
|
|
132
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "X-API-Key": this.config.apiKey },
|
|
135
|
+
body: form,
|
|
136
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
137
|
+
}).then((res) => this.json(res));
|
|
138
|
+
}
|
|
139
|
+
/** Preserves named events, multiline data and UTF-8 split across chunks. */
|
|
140
|
+
async *events(path, opts = {}) {
|
|
141
|
+
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
142
|
+
method: opts.method ?? "GET",
|
|
143
|
+
headers: this.config.headers,
|
|
144
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
145
|
+
signal: opts.signal
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) await handleApiError(res);
|
|
148
|
+
if (!res.body) return;
|
|
149
|
+
const reader = res.body.getReader(), decoder = new TextDecoder();
|
|
150
|
+
let buffer = "", event = "message", id, data = [];
|
|
151
|
+
const line = (text) => {
|
|
152
|
+
if (text === "") {
|
|
153
|
+
const result = data.length ? { event, data: data.join("\n"), id } : void 0;
|
|
154
|
+
event = "message";
|
|
155
|
+
data = [];
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
if (text.startsWith(":")) return;
|
|
159
|
+
const colon = text.indexOf(":");
|
|
160
|
+
const field = colon < 0 ? text : text.slice(0, colon);
|
|
161
|
+
const value = colon < 0 ? "" : text.slice(colon + 1).replace(/^ /, "");
|
|
162
|
+
if (field === "data") data.push(value);
|
|
163
|
+
if (field === "event") event = value;
|
|
164
|
+
if (field === "id" && !value.includes("\0")) id = value;
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
while (true) {
|
|
168
|
+
const { done, value } = await reader.read();
|
|
169
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
170
|
+
let end;
|
|
171
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
172
|
+
const result = line(buffer.slice(0, end).replace(/\r$/, ""));
|
|
173
|
+
buffer = buffer.slice(end + 1);
|
|
174
|
+
if (result) yield result;
|
|
175
|
+
}
|
|
176
|
+
if (done) break;
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
await reader.cancel();
|
|
180
|
+
reader.releaseLock();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async *streamSse(path) {
|
|
184
|
+
for await (const event of this.events(path)) yield event.data;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/platform/storage.ts
|
|
189
|
+
var StorageAPI = class {
|
|
190
|
+
constructor(client) {
|
|
191
|
+
this.client = client;
|
|
192
|
+
}
|
|
193
|
+
list(projectId, addonId, opts = {}) {
|
|
194
|
+
return this.client.get(query(`/api/projects/${projectId}/addons/${addonId}/s3/buckets/${segment(opts.bucket ?? "default")}/objects`, { prefix: opts.prefix }));
|
|
195
|
+
}
|
|
196
|
+
upload(projectId, addonId, opts) {
|
|
197
|
+
const key = opts.key.split("/").map(segment).join("/");
|
|
198
|
+
return this.client.sendBytes("PUT", `/api/projects/${projectId}/addons/${addonId}/s3/objects/${segment(opts.bucket ?? "default")}/${key}`, opts.source, opts.contentType);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/platform/services.ts
|
|
203
|
+
var DeployHandle = class {
|
|
204
|
+
constructor(client, serviceId, buildId) {
|
|
205
|
+
this._client = client;
|
|
206
|
+
this._serviceId = serviceId;
|
|
207
|
+
this._buildId = buildId;
|
|
208
|
+
}
|
|
209
|
+
get serviceId() {
|
|
210
|
+
return this._serviceId;
|
|
211
|
+
}
|
|
212
|
+
/** Stream build log messages until the server closes the build stream. */
|
|
213
|
+
async *logs() {
|
|
214
|
+
let buildId = this._buildId;
|
|
215
|
+
if (!buildId) {
|
|
216
|
+
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
217
|
+
buildId = svc.builds?.[0]?.id;
|
|
218
|
+
}
|
|
219
|
+
if (!buildId) throw new LizardError("No build found for this deploy");
|
|
220
|
+
for await (const event of this._client.events(`/api/builds/${buildId}/logs`)) {
|
|
221
|
+
if (event.event === "error") throw new LizardError(event.data);
|
|
222
|
+
if (event.event === "done") return;
|
|
223
|
+
let data;
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(event.data);
|
|
226
|
+
} catch {
|
|
227
|
+
data = event.data;
|
|
228
|
+
}
|
|
229
|
+
yield typeof data === "string" ? data : event.data;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Poll until the deploy reaches a terminal state.
|
|
234
|
+
* @returns `{ url, status }` on success; throws `LizardError` on failure.
|
|
235
|
+
*/
|
|
236
|
+
async wait(opts) {
|
|
237
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 10 * 6e4);
|
|
238
|
+
const pollMs = opts?.pollMs ?? 3e3;
|
|
239
|
+
while (Date.now() < deadline) {
|
|
240
|
+
let buildDone = true;
|
|
241
|
+
if (this._buildId) {
|
|
242
|
+
const build = await this._client.get(`/api/builds/${this._buildId}`);
|
|
243
|
+
if (build.status === "failed" || build.status === "cancelled") throw new LizardError(`Deploy ${build.status}`);
|
|
244
|
+
buildDone = build.status === "done";
|
|
245
|
+
}
|
|
246
|
+
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
247
|
+
if (svc.deployStatus === "idle") {
|
|
248
|
+
if (buildDone && svc.status === "running") return { url: svc.domain ? `https://${svc.domain}` : null, status: "running" };
|
|
249
|
+
if (svc.status === "crashed") throw new LizardError(`Service crashed after deploy`);
|
|
250
|
+
}
|
|
251
|
+
if (svc.deployStatus === "failed") throw new LizardError(`Deploy failed`);
|
|
252
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
253
|
+
}
|
|
254
|
+
throw new TimeoutError(`Deploy did not complete within ${opts?.timeoutMs ?? 6e5}ms`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var ServicesAPI = class {
|
|
258
|
+
constructor(client) {
|
|
259
|
+
this.client = client;
|
|
260
|
+
}
|
|
261
|
+
/** List all services in a project. */
|
|
262
|
+
list(opts) {
|
|
263
|
+
return this.client.get(`/api/projects/${opts.projectId}/apps`);
|
|
264
|
+
}
|
|
265
|
+
/** Get a service by ID. */
|
|
266
|
+
get(id) {
|
|
267
|
+
return this.client.get(`/api/apps/${id}`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Create a service and start a deploy from a git repo.
|
|
271
|
+
* Returns a DeployHandle to track progress.
|
|
272
|
+
*/
|
|
273
|
+
async deploy(opts) {
|
|
274
|
+
const body = {
|
|
275
|
+
name: opts.name,
|
|
276
|
+
sourceType: opts.sourceType ?? "github",
|
|
277
|
+
repoUrl: opts.repoUrl,
|
|
278
|
+
branch: opts.branch ?? "main",
|
|
279
|
+
startCommand: opts.startCommand,
|
|
280
|
+
buildCommand: opts.buildCommand,
|
|
281
|
+
preDeployCommand: opts.preDeployCommand,
|
|
282
|
+
dockerfilePath: opts.dockerfilePath,
|
|
283
|
+
context: opts.rootDirectory,
|
|
284
|
+
envVars: opts.envVars,
|
|
285
|
+
skipInitialDeploy: opts.skipInitialDeploy,
|
|
286
|
+
cpuLimit: opts.cpuLimit,
|
|
287
|
+
memoryLimit: opts.memoryLimit,
|
|
288
|
+
containerPort: opts.port ?? opts.containerPort,
|
|
289
|
+
region: opts.region
|
|
290
|
+
};
|
|
291
|
+
const svc = await this.client.post(`/api/projects/${opts.projectId}/apps`, body);
|
|
292
|
+
const handle = new DeployHandle(this.client, svc.id, svc.buildId);
|
|
293
|
+
if (opts.waitForDeploy) await handle.wait();
|
|
294
|
+
return handle;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Upload a tarball and deploy it.
|
|
298
|
+
*
|
|
299
|
+
* @param opts.source - A `Buffer`, `Blob`, or `Uint8Array` of a `.tar.gz` file.
|
|
300
|
+
*/
|
|
301
|
+
async upload(opts) {
|
|
302
|
+
if (!opts.name && !opts.serviceId) throw new LizardError("name or serviceId is required");
|
|
303
|
+
const path = query(`/api/projects/${opts.projectId}/apps/upload`, {
|
|
304
|
+
name: opts.name,
|
|
305
|
+
appId: opts.serviceId,
|
|
306
|
+
startCommand: opts.startCommand,
|
|
307
|
+
buildCommand: opts.buildCommand,
|
|
308
|
+
preDeployCommand: opts.preDeployCommand,
|
|
309
|
+
port: opts.port,
|
|
310
|
+
region: opts.region
|
|
311
|
+
});
|
|
312
|
+
const result = await this.client.sendBytes("POST", path, opts.source);
|
|
313
|
+
return new DeployHandle(this.client, result.id, result.buildId);
|
|
314
|
+
}
|
|
315
|
+
/** Trigger a redeploy (rebuild from current source). */
|
|
316
|
+
async redeploy(id) {
|
|
317
|
+
const build = await this.client.post(`/api/apps/${id}/redeploy`, {});
|
|
318
|
+
return new DeployHandle(this.client, id, build.id);
|
|
319
|
+
}
|
|
320
|
+
/** Restart a service without rebuilding. */
|
|
321
|
+
restart(id) {
|
|
322
|
+
return this.client.post(`/api/apps/${id}/restart`, {});
|
|
323
|
+
}
|
|
324
|
+
/** Scale a service. */
|
|
325
|
+
async scale(id, opts) {
|
|
326
|
+
if (opts.storageMi !== void 0) throw new LizardError("Storage scaling is only supported for addons");
|
|
327
|
+
if (opts.replicas !== void 0) await this.client.patch(`/api/apps/${id}/scale`, { replicas: opts.replicas });
|
|
328
|
+
if (opts.cpuMillis !== void 0 || opts.memoryMi !== void 0) {
|
|
329
|
+
await this.update(id, {
|
|
330
|
+
...opts.cpuMillis !== void 0 ? { cpuLimit: `${opts.cpuMillis}m` } : {},
|
|
331
|
+
...opts.memoryMi !== void 0 ? { memoryLimit: `${opts.memoryMi}Mi` } : {}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get recent log lines. For a live tail, use the WebSocket API instead.
|
|
337
|
+
*
|
|
338
|
+
* @param opts.limit - Max lines to return (default 200, max 1000).
|
|
339
|
+
*/
|
|
340
|
+
async logs(id, opts) {
|
|
341
|
+
const svc = await this.get(id);
|
|
342
|
+
const result = await this.client.get(query(`/api/projects/${svc.projectId}/logs`, { service: svc.name, limit: opts?.limit ?? 200, since: opts?.since }));
|
|
343
|
+
return Array.isArray(result) ? result : result.logs ?? [];
|
|
344
|
+
}
|
|
345
|
+
/** Execute a command; collect SSE output and require an exit event. */
|
|
346
|
+
async exec(id, cmd, opts) {
|
|
347
|
+
const result = { stdout: "", stderr: "", exitCode: -1 };
|
|
348
|
+
for await (const event of this.client.events(`/api/apps/${id}/exec`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: { cmd },
|
|
351
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 3e5)
|
|
352
|
+
})) {
|
|
353
|
+
if (event.event === "error") throw new LizardError(event.data);
|
|
354
|
+
const data = JSON.parse(event.data);
|
|
355
|
+
if (data.stream === "stdout" || data.stream === "stderr") result[data.stream] += (data.line ?? "") + "\n";
|
|
356
|
+
if (event.event === "exit" && typeof data.exitCode === "number") result.exitCode = data.exitCode;
|
|
357
|
+
}
|
|
358
|
+
if (result.exitCode === -1) throw new LizardError("Exec stream ended without exit status");
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
async update(id, opts, controls = {}) {
|
|
362
|
+
const svc = await this.get(id);
|
|
363
|
+
let revision = controls.revision;
|
|
364
|
+
if (!controls.force && revision === void 0) {
|
|
365
|
+
const project = await this.client.get(`/api/projects/${svc.projectId}`);
|
|
366
|
+
revision = project.configRevision;
|
|
367
|
+
}
|
|
368
|
+
return this.client.applyConfig(svc.projectId, {
|
|
369
|
+
...controls.force ? {} : { revision },
|
|
370
|
+
services: [{ name: svc.name, ...opts, id }]
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
events(id, buildId) {
|
|
374
|
+
return this.client.get(query(`/api/apps/${id}/deploy-events`, { buildId }));
|
|
375
|
+
}
|
|
376
|
+
pods(id) {
|
|
377
|
+
return this.client.get(`/api/apps/${id}/pod-status`);
|
|
378
|
+
}
|
|
379
|
+
history(id, opts = {}) {
|
|
380
|
+
return this.client.get(query(`/api/apps/${id}/logs/history`, opts));
|
|
381
|
+
}
|
|
382
|
+
streamLogs(id, signal) {
|
|
383
|
+
return this.client.events(`/api/apps/${id}/logs`, { signal });
|
|
384
|
+
}
|
|
385
|
+
buildLogs(buildId, signal) {
|
|
386
|
+
return this.client.events(`/api/builds/${buildId}/logs`, { signal });
|
|
387
|
+
}
|
|
388
|
+
/** Delete a service. */
|
|
389
|
+
delete(id) {
|
|
390
|
+
return this.client.delete(`/api/apps/${id}`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/platform/github.ts
|
|
395
|
+
var GitHubAPI = class {
|
|
396
|
+
constructor(client) {
|
|
397
|
+
this.client = client;
|
|
398
|
+
}
|
|
399
|
+
status() {
|
|
400
|
+
return this.client.get("/api/github/status");
|
|
401
|
+
}
|
|
402
|
+
/** Complete the GitHub App installation in a browser using the caller's account. */
|
|
403
|
+
installUrl() {
|
|
404
|
+
return `${this.client.config.apiUrl}/api/auth/github/install`;
|
|
405
|
+
}
|
|
406
|
+
async checkout(serviceId, branch) {
|
|
407
|
+
const services = new ServicesAPI(this.client);
|
|
408
|
+
await services.update(serviceId, { branch });
|
|
409
|
+
return services.redeploy(serviceId);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
69
413
|
// src/project.ts
|
|
414
|
+
import { createHash } from "crypto";
|
|
70
415
|
var projectIdCache = /* @__PURE__ */ new Map();
|
|
71
416
|
async function resolveProjectId(ref, config) {
|
|
72
|
-
const cacheKey = `${config.apiUrl}\0${ref}`;
|
|
417
|
+
const cacheKey = `${config.apiUrl}\0${createHash("sha256").update(config.apiKey).digest("hex")}\0${ref}`;
|
|
73
418
|
const cached = projectIdCache.get(cacheKey);
|
|
74
419
|
if (cached) return cached;
|
|
75
420
|
const res = await fetch(`${config.apiUrl}/api/projects`, { headers: config.headers });
|
|
76
421
|
if (!res.ok) await handleApiError(res);
|
|
77
422
|
const projects = await res.json();
|
|
78
423
|
const lower = ref.toLowerCase();
|
|
79
|
-
const
|
|
424
|
+
const matches = projects.filter(
|
|
80
425
|
(p) => p.id.toLowerCase() === lower || p.slug?.toLowerCase() === lower || p.name?.toLowerCase() === lower
|
|
81
426
|
);
|
|
427
|
+
if (matches.length > 1) throw new LizardError("Project reference is ambiguous; pass the project ID");
|
|
428
|
+
const match = matches[0];
|
|
82
429
|
if (!match) {
|
|
83
430
|
const available = projects.map((p) => p.slug || p.id).join(", ") || "(none)";
|
|
84
431
|
throw new LizardError(
|
|
@@ -103,12 +450,12 @@ var Process = class {
|
|
|
103
450
|
this.config = config;
|
|
104
451
|
}
|
|
105
452
|
/**
|
|
106
|
-
* Execute a command inside the
|
|
453
|
+
* Execute a command inside the sandbox and wait for it to complete.
|
|
107
454
|
*
|
|
108
455
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
109
456
|
* stdout, stderr, and the exit code when it finishes.
|
|
110
457
|
*
|
|
111
|
-
* @param cmd Shell command to run inside the
|
|
458
|
+
* @param cmd Shell command to run inside the sandbox.
|
|
112
459
|
* @param opts Optional execution options — environment variables, working
|
|
113
460
|
* directory, user, and timeout.
|
|
114
461
|
*
|
|
@@ -240,22 +587,23 @@ var Fs = class {
|
|
|
240
587
|
this.config = config;
|
|
241
588
|
}
|
|
242
589
|
/**
|
|
243
|
-
* Write a file into the
|
|
590
|
+
* Write a file into the sandbox filesystem.
|
|
244
591
|
*
|
|
245
592
|
* Creates parent directories automatically if they don't exist.
|
|
593
|
+
* Byte input must contain valid UTF-8; arbitrary binary uploads are unsupported.
|
|
246
594
|
*
|
|
247
595
|
* @example
|
|
248
596
|
* ```ts
|
|
249
597
|
* await sandbox.fs.write('/app/index.js', 'console.log("hello")')
|
|
250
598
|
* ```
|
|
251
599
|
*
|
|
252
|
-
* @example Write
|
|
600
|
+
* @example Write UTF-8 bytes:
|
|
253
601
|
* ```ts
|
|
254
|
-
* await sandbox.fs.write('/app/
|
|
602
|
+
* await sandbox.fs.write('/app/notes.txt', new TextEncoder().encode('hello'))
|
|
255
603
|
* ```
|
|
256
604
|
*/
|
|
257
605
|
async write(path, data, opts) {
|
|
258
|
-
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
606
|
+
const content = typeof data === "string" ? data : new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
259
607
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
260
608
|
method: "POST",
|
|
261
609
|
headers: this.config.headers,
|
|
@@ -264,7 +612,7 @@ var Fs = class {
|
|
|
264
612
|
if (!res.ok) await handleApiError(res);
|
|
265
613
|
}
|
|
266
614
|
/**
|
|
267
|
-
* Read a file from the
|
|
615
|
+
* Read a file from the sandbox filesystem.
|
|
268
616
|
*
|
|
269
617
|
* @returns The file contents as a UTF-8 string.
|
|
270
618
|
*
|
|
@@ -281,8 +629,17 @@ var Fs = class {
|
|
|
281
629
|
if (!res.ok) await handleApiError(res);
|
|
282
630
|
return res.text();
|
|
283
631
|
}
|
|
632
|
+
/** Read a file as bytes. */
|
|
633
|
+
async readBytes(path, opts) {
|
|
634
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`);
|
|
635
|
+
url.searchParams.set("path", path);
|
|
636
|
+
if (opts?.user) url.searchParams.set("user", opts.user);
|
|
637
|
+
const res = await fetch(url, { headers: this.config.headers });
|
|
638
|
+
if (!res.ok) await handleApiError(res);
|
|
639
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
640
|
+
}
|
|
284
641
|
/**
|
|
285
|
-
* List files and directories at the given path inside the
|
|
642
|
+
* List files and directories at the given path inside the sandbox.
|
|
286
643
|
*
|
|
287
644
|
* @example
|
|
288
645
|
* ```ts
|
|
@@ -298,7 +655,7 @@ var Fs = class {
|
|
|
298
655
|
return res.json();
|
|
299
656
|
}
|
|
300
657
|
/**
|
|
301
|
-
* Remove a file or directory from the
|
|
658
|
+
* Remove a file or directory from the sandbox filesystem.
|
|
302
659
|
*/
|
|
303
660
|
async remove(path, opts) {
|
|
304
661
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
@@ -309,7 +666,7 @@ var Fs = class {
|
|
|
309
666
|
if (!res.ok) await handleApiError(res);
|
|
310
667
|
}
|
|
311
668
|
/**
|
|
312
|
-
* Create a directory (and any missing parents) inside the
|
|
669
|
+
* Create a directory (and any missing parents) inside the sandbox.
|
|
313
670
|
*/
|
|
314
671
|
async makeDir(path, opts) {
|
|
315
672
|
await this.execInternal(`mkdir -p ${JSON.stringify(path)}`, opts?.user);
|
|
@@ -480,7 +837,8 @@ var SandboxClient = class {
|
|
|
480
837
|
}
|
|
481
838
|
static async listSandboxes(opts) {
|
|
482
839
|
const config = new ConnectionConfig(opts);
|
|
483
|
-
const
|
|
840
|
+
const path = opts?.projectId ? `/api/projects/${encodeURIComponent(opts.projectId)}/sandboxes` : "/api/sandboxes";
|
|
841
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
484
842
|
headers: config.headers
|
|
485
843
|
});
|
|
486
844
|
if (!res.ok) await handleApiError(res);
|
|
@@ -536,11 +894,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
536
894
|
* Verifies the sandbox exists and is reachable, then returns a handle to it.
|
|
537
895
|
* Throws `NotFoundError` if it has been killed or has expired.
|
|
538
896
|
*
|
|
539
|
-
*
|
|
540
|
-
* reconnecting to might be paused. Sandboxes are pods now and pause/resume is a
|
|
541
|
-
* `501` on every one of them, so that call turned every `connect()` into an
|
|
542
|
-
* error against a perfectly healthy sandbox. Connecting does not need to change
|
|
543
|
-
* a sandbox's state, so it no longer tries to.
|
|
897
|
+
* Connecting reads sandbox metadata without changing its state.
|
|
544
898
|
*
|
|
545
899
|
* @example
|
|
546
900
|
* ```ts
|
|
@@ -565,29 +919,25 @@ var Sandbox = class extends SandboxClient {
|
|
|
565
919
|
/**
|
|
566
920
|
* Kill the sandbox and release its resources immediately.
|
|
567
921
|
*
|
|
568
|
-
* @returns `true` if the
|
|
922
|
+
* @returns `true` if the sandbox was terminated, `false` if it was already gone.
|
|
569
923
|
*/
|
|
570
924
|
async kill(opts) {
|
|
571
925
|
return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
572
926
|
}
|
|
573
927
|
/**
|
|
574
|
-
*
|
|
928
|
+
* Request sandbox pause. The Kubernetes backend returns HTTP 501.
|
|
575
929
|
*
|
|
576
|
-
* @deprecated
|
|
577
|
-
*
|
|
578
|
-
* checkpoint of the pod, which is not built. To park work across a gap, put it on a
|
|
579
|
-
* {@link Volume} and create a fresh sandbox on that volume later; the volume is the
|
|
580
|
-
* part that is meant to outlive a sandbox.
|
|
930
|
+
* @deprecated Unsupported on Kubernetes. Save files to a {@link Volume}
|
|
931
|
+
* and attach it to a new sandbox instead.
|
|
581
932
|
*/
|
|
582
933
|
async pause(opts) {
|
|
583
934
|
return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
584
935
|
}
|
|
585
936
|
/**
|
|
586
|
-
*
|
|
937
|
+
* Request sandbox resume. The Kubernetes backend returns HTTP 501.
|
|
587
938
|
*
|
|
588
|
-
* @deprecated
|
|
589
|
-
*
|
|
590
|
-
* calls this, so reconnecting to a running sandbox works without it.
|
|
939
|
+
* @deprecated Unsupported on Kubernetes. Use {@link Sandbox.connect} to
|
|
940
|
+
* reconnect to a running sandbox.
|
|
591
941
|
*/
|
|
592
942
|
async resume(opts) {
|
|
593
943
|
return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
@@ -607,102 +957,53 @@ var Sandbox = class extends SandboxClient {
|
|
|
607
957
|
return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
|
|
608
958
|
}
|
|
609
959
|
/**
|
|
610
|
-
*
|
|
960
|
+
* Register a public HTTPS route and return its hostname without a scheme.
|
|
611
961
|
*
|
|
612
|
-
* Useful for accessing HTTP servers started inside the
|
|
962
|
+
* Useful for accessing HTTP servers started inside the sandbox from your
|
|
613
963
|
* agent or tests without additional tunneling.
|
|
614
964
|
*
|
|
615
965
|
* @example
|
|
616
966
|
* ```ts
|
|
617
|
-
*
|
|
618
|
-
* const
|
|
619
|
-
*
|
|
967
|
+
* // Start an HTTP server on port 3000 before exposing it.
|
|
968
|
+
* const hostname = await sandbox.getHost(3000)
|
|
969
|
+
* console.log(`https://${hostname}`)
|
|
620
970
|
* ```
|
|
621
971
|
*/
|
|
622
972
|
async getHost(port, opts) {
|
|
623
973
|
const { hostname } = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
|
|
624
974
|
return hostname;
|
|
625
975
|
}
|
|
626
|
-
|
|
627
|
-
|
|
976
|
+
/** Fork is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
977
|
+
fork(opts = {}) {
|
|
978
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/fork`, { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 });
|
|
628
979
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// src/platform/client.ts
|
|
634
|
-
var PlatformClient = class {
|
|
635
|
-
constructor(opts) {
|
|
636
|
-
this.config = new ConnectionConfig(opts);
|
|
980
|
+
/** Snapshot creation is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
981
|
+
snapshot(name) {
|
|
982
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name });
|
|
637
983
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
headers: this.config.headers
|
|
641
|
-
});
|
|
642
|
-
if (!res.ok) await handleApiError(res);
|
|
643
|
-
return res.json();
|
|
984
|
+
unexpose(port) {
|
|
985
|
+
return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
|
|
644
986
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
method: "POST",
|
|
648
|
-
headers: this.config.headers,
|
|
649
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
650
|
-
});
|
|
651
|
-
if (!res.ok) await handleApiError(res);
|
|
652
|
-
return res.json();
|
|
987
|
+
logs(opts = {}) {
|
|
988
|
+
return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
|
|
653
989
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
method: "PATCH",
|
|
657
|
-
headers: this.config.headers,
|
|
658
|
-
body: JSON.stringify(body)
|
|
659
|
-
});
|
|
660
|
-
if (!res.ok) await handleApiError(res);
|
|
661
|
-
return res.json();
|
|
990
|
+
static snapshots(projectId, opts) {
|
|
991
|
+
return new PlatformClient(opts ?? {}).get(`/api/projects/${projectId}/snapshots`);
|
|
662
992
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
668
|
-
});
|
|
669
|
-
if (!res.ok) await handleApiError(res);
|
|
670
|
-
if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
|
|
671
|
-
return res.json();
|
|
993
|
+
/** Snapshot restore is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
994
|
+
static async restore(snapshotId, opts) {
|
|
995
|
+
const result = await new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/fork`, { timeoutMs: opts?.timeoutMs ?? 0 });
|
|
996
|
+
return new this({ ...opts, sandboxId: result.sandboxId ?? result.id });
|
|
672
997
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
676
|
-
method: "POST",
|
|
677
|
-
headers,
|
|
678
|
-
body: form
|
|
679
|
-
});
|
|
680
|
-
if (!res.ok) await handleApiError(res);
|
|
681
|
-
return res.json();
|
|
998
|
+
static deleteSnapshot(snapshotId, opts) {
|
|
999
|
+
return new PlatformClient(opts ?? {}).delete(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
682
1000
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
686
|
-
headers: this.config.headers
|
|
687
|
-
});
|
|
688
|
-
if (!res.ok) await handleApiError(res);
|
|
689
|
-
if (!res.body) return;
|
|
690
|
-
const reader = res.body.getReader();
|
|
691
|
-
const decoder = new TextDecoder();
|
|
692
|
-
let buf = "";
|
|
693
|
-
while (true) {
|
|
694
|
-
const { done, value } = await reader.read();
|
|
695
|
-
if (done) break;
|
|
696
|
-
buf += decoder.decode(value, { stream: true });
|
|
697
|
-
const events = buf.split("\n\n");
|
|
698
|
-
buf = events.pop();
|
|
699
|
-
for (const evt of events) {
|
|
700
|
-
const dataLine = evt.split("\n").find((l) => l.startsWith("data: "));
|
|
701
|
-
if (dataLine) yield dataLine.slice(6);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
1001
|
+
resolveOpts(opts) {
|
|
1002
|
+
return { ...this.connectionConfig, ...opts };
|
|
704
1003
|
}
|
|
705
1004
|
};
|
|
1005
|
+
Sandbox.defaultTemplate = "base";
|
|
1006
|
+
Sandbox.defaultTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
706
1007
|
|
|
707
1008
|
// src/platform/projects.ts
|
|
708
1009
|
var ProjectsAPI = class {
|
|
@@ -730,134 +1031,20 @@ var ProjectsAPI = class {
|
|
|
730
1031
|
delete(id) {
|
|
731
1032
|
return this.client.delete(`/api/projects/${id}`);
|
|
732
1033
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
// src/platform/services.ts
|
|
736
|
-
var DeployHandle = class {
|
|
737
|
-
constructor(client, serviceId, buildId) {
|
|
738
|
-
this._client = client;
|
|
739
|
-
this._serviceId = serviceId;
|
|
740
|
-
this._buildId = buildId;
|
|
741
|
-
}
|
|
742
|
-
get serviceId() {
|
|
743
|
-
return this._serviceId;
|
|
1034
|
+
apply(id, config) {
|
|
1035
|
+
return this.client.applyConfig(id, config);
|
|
744
1036
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const path = `/api/apps/${this._serviceId}/logs?limit=1000`;
|
|
748
|
-
const res = await fetch(`${this._client.config.apiUrl}${path}`, {
|
|
749
|
-
headers: this._client.config.headers
|
|
750
|
-
});
|
|
751
|
-
if (!res.ok) return;
|
|
752
|
-
const body = await res.json();
|
|
753
|
-
const lines = Array.isArray(body) ? body : body.logs ?? [];
|
|
754
|
-
for (const l of lines) yield typeof l === "string" ? l : JSON.stringify(l);
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Poll until the deploy reaches a terminal state.
|
|
758
|
-
* @returns `{ url, status }` on success; throws `LizardError` on failure.
|
|
759
|
-
*/
|
|
760
|
-
async wait(opts) {
|
|
761
|
-
const deadline = Date.now() + (opts?.timeoutMs ?? 10 * 6e4);
|
|
762
|
-
const pollMs = opts?.pollMs ?? 3e3;
|
|
763
|
-
while (Date.now() < deadline) {
|
|
764
|
-
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
765
|
-
if (svc.deployStatus === "idle") {
|
|
766
|
-
if (svc.status === "running") return { url: svc.domain ? `https://${svc.domain}` : null, status: "running" };
|
|
767
|
-
if (svc.status === "crashed") throw new LizardError(`Service crashed after deploy`);
|
|
768
|
-
}
|
|
769
|
-
if (svc.deployStatus === "failed") throw new LizardError(`Deploy failed`);
|
|
770
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
771
|
-
}
|
|
772
|
-
throw new TimeoutError(`Deploy did not complete within ${opts?.timeoutMs ?? 6e5}ms`);
|
|
1037
|
+
services(id) {
|
|
1038
|
+
return this.client.get(`/api/projects/${id}/services`);
|
|
773
1039
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
constructor(client) {
|
|
777
|
-
this.client = client;
|
|
1040
|
+
volumeLimits(id) {
|
|
1041
|
+
return this.client.get(`/api/projects/${id}/volume-limits`);
|
|
778
1042
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return this.client.get(`/api/projects/${opts.projectId}/apps`);
|
|
1043
|
+
logs(id, opts = {}) {
|
|
1044
|
+
return this.client.get(query(`/api/projects/${id}/logs`, opts));
|
|
782
1045
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return this.client.get(`/api/apps/${id}`);
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Create a service and start a deploy from a git repo.
|
|
789
|
-
* Returns a DeployHandle to track progress.
|
|
790
|
-
*/
|
|
791
|
-
async deploy(opts) {
|
|
792
|
-
const body = {
|
|
793
|
-
name: opts.name,
|
|
794
|
-
sourceType: opts.sourceType ?? "github",
|
|
795
|
-
repoUrl: opts.repoUrl,
|
|
796
|
-
branch: opts.branch ?? "main",
|
|
797
|
-
startCommand: opts.startCommand,
|
|
798
|
-
buildCommand: opts.buildCommand,
|
|
799
|
-
containerPort: opts.port ?? opts.containerPort,
|
|
800
|
-
region: opts.region
|
|
801
|
-
};
|
|
802
|
-
const svc = await this.client.post(`/api/projects/${opts.projectId}/apps`, body);
|
|
803
|
-
return new DeployHandle(this.client, svc.id);
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Upload a tarball and deploy it.
|
|
807
|
-
*
|
|
808
|
-
* @param opts.source - A `Buffer`, `Blob`, or `Uint8Array` of a `.tar.gz` file.
|
|
809
|
-
*/
|
|
810
|
-
async upload(opts) {
|
|
811
|
-
const form = new FormData();
|
|
812
|
-
const blob = opts.source instanceof Blob ? opts.source : new Blob([opts.source], { type: "application/gzip" });
|
|
813
|
-
form.append("file", blob, "source.tar.gz");
|
|
814
|
-
form.append("name", opts.name);
|
|
815
|
-
if (opts.startCommand) form.append("startCommand", opts.startCommand);
|
|
816
|
-
if (opts.buildCommand) form.append("buildCommand", opts.buildCommand);
|
|
817
|
-
if (opts.port !== void 0) form.append("containerPort", String(opts.port));
|
|
818
|
-
if (opts.region) form.append("region", opts.region);
|
|
819
|
-
const result = await this.client.postForm(
|
|
820
|
-
`/api/projects/${opts.projectId}/apps/upload`,
|
|
821
|
-
form
|
|
822
|
-
);
|
|
823
|
-
return new DeployHandle(this.client, result.id, result.buildId);
|
|
824
|
-
}
|
|
825
|
-
/** Trigger a redeploy (rebuild from current source). */
|
|
826
|
-
async redeploy(id) {
|
|
827
|
-
await this.client.post(`/api/apps/${id}/redeploy`, {});
|
|
828
|
-
return new DeployHandle(this.client, id);
|
|
829
|
-
}
|
|
830
|
-
/** Restart a service without rebuilding. */
|
|
831
|
-
restart(id) {
|
|
832
|
-
return this.client.post(`/api/apps/${id}/restart`, {});
|
|
833
|
-
}
|
|
834
|
-
/** Scale a service. */
|
|
835
|
-
scale(id, opts) {
|
|
836
|
-
return this.client.post(`/api/apps/${id}/scale`, opts);
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Get recent log lines. For a live tail, use the WebSocket API instead.
|
|
840
|
-
*
|
|
841
|
-
* @param opts.limit - Max lines to return (default 200, max 1000).
|
|
842
|
-
*/
|
|
843
|
-
async logs(id, opts) {
|
|
844
|
-
const qs = new URLSearchParams();
|
|
845
|
-
if (opts?.limit) qs.set("limit", String(opts.limit));
|
|
846
|
-
if (opts?.since) qs.set("since", opts.since);
|
|
847
|
-
const result = await this.client.get(`/api/apps/${id}/logs?${qs}`);
|
|
848
|
-
return Array.isArray(result) ? result : result.logs ?? [];
|
|
849
|
-
}
|
|
850
|
-
/** Execute a command inside the running service container. */
|
|
851
|
-
exec(id, cmd, opts) {
|
|
852
|
-
return this.client.post(`/api/apps/${id}/exec`, { cmd, timeoutMs: opts?.timeoutMs });
|
|
853
|
-
}
|
|
854
|
-
/** Update service configuration. */
|
|
855
|
-
update(id, opts) {
|
|
856
|
-
return this.client.patch(`/api/apps/${id}`, opts);
|
|
857
|
-
}
|
|
858
|
-
/** Delete a service. */
|
|
859
|
-
delete(id) {
|
|
860
|
-
return this.client.delete(`/api/apps/${id}`);
|
|
1046
|
+
streamLogs(id, signal) {
|
|
1047
|
+
return this.client.events(`/api/projects/${id}/logs/stream`, { signal });
|
|
861
1048
|
}
|
|
862
1049
|
};
|
|
863
1050
|
|
|
@@ -872,7 +1059,11 @@ var AddonsAPI = class {
|
|
|
872
1059
|
}
|
|
873
1060
|
/** Get an addon by ID. */
|
|
874
1061
|
get(projectId, addonId) {
|
|
875
|
-
return this.
|
|
1062
|
+
return this.list({ projectId }).then((items) => {
|
|
1063
|
+
const addon = items.find((a) => a.id === addonId);
|
|
1064
|
+
if (!addon) throw new Error("Addon not found");
|
|
1065
|
+
return addon;
|
|
1066
|
+
});
|
|
876
1067
|
}
|
|
877
1068
|
/**
|
|
878
1069
|
* Create a new managed addon (database, cache, or object store).
|
|
@@ -886,12 +1077,15 @@ var AddonsAPI = class {
|
|
|
886
1077
|
*/
|
|
887
1078
|
create(opts) {
|
|
888
1079
|
return this.client.post(`/api/projects/${opts.projectId}/addons`, {
|
|
889
|
-
type: opts.type,
|
|
1080
|
+
type: opts.type === "mongodb" ? "mongo" : opts.type,
|
|
890
1081
|
name: opts.name,
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1082
|
+
region: opts.region,
|
|
1083
|
+
config: {
|
|
1084
|
+
version: opts.version,
|
|
1085
|
+
cpuLimit: opts.vcpu === void 0 ? void 0 : `${opts.vcpu * 1e3}m`,
|
|
1086
|
+
memoryLimit: opts.memoryMb === void 0 ? void 0 : `${opts.memoryMb}Mi`,
|
|
1087
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1088
|
+
}
|
|
895
1089
|
});
|
|
896
1090
|
}
|
|
897
1091
|
/** Delete an addon. */
|
|
@@ -899,13 +1093,29 @@ var AddonsAPI = class {
|
|
|
899
1093
|
return this.client.delete(`/api/projects/${projectId}/addons/${addonId}`);
|
|
900
1094
|
}
|
|
901
1095
|
/** Resize an addon (CPU / memory / storage). */
|
|
902
|
-
resize(projectId, addonId, opts) {
|
|
903
|
-
|
|
1096
|
+
async resize(projectId, addonId, opts) {
|
|
1097
|
+
await this.client.applyConfig(projectId, {
|
|
1098
|
+
addons: [{
|
|
1099
|
+
id: addonId,
|
|
1100
|
+
limits: { vcpu: opts.vcpu, memoryMb: opts.memoryMb },
|
|
1101
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1102
|
+
}]
|
|
1103
|
+
});
|
|
1104
|
+
return this.get(projectId, addonId);
|
|
904
1105
|
}
|
|
905
|
-
/** Restart an addon
|
|
1106
|
+
/** Restart an addon. */
|
|
906
1107
|
redeploy(projectId, addonId) {
|
|
907
1108
|
return this.client.post(`/api/projects/${projectId}/addons/${addonId}/redeploy`, {});
|
|
908
1109
|
}
|
|
1110
|
+
rename(projectId, addonId, name) {
|
|
1111
|
+
return this.client.patch(`/api/projects/${projectId}/addons/${addonId}`, { name });
|
|
1112
|
+
}
|
|
1113
|
+
secrets(projectId, addonId) {
|
|
1114
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/secrets`);
|
|
1115
|
+
}
|
|
1116
|
+
logs(projectId, addonId) {
|
|
1117
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/logs`);
|
|
1118
|
+
}
|
|
909
1119
|
};
|
|
910
1120
|
|
|
911
1121
|
// src/platform/secrets.ts
|
|
@@ -948,22 +1158,25 @@ var SecretsAPI = class {
|
|
|
948
1158
|
byService.get(s.serviceId)[s.key] = s.value;
|
|
949
1159
|
}
|
|
950
1160
|
}
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
1161
|
+
const services = {};
|
|
1162
|
+
for (const [id, values] of byService) {
|
|
1163
|
+
const svc = await this.client.get(`/api/apps/${id}`);
|
|
1164
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1165
|
+
services[svc.name] = values;
|
|
954
1166
|
}
|
|
955
|
-
|
|
956
|
-
tasks.push(this.client.post(`/api/apps/${serviceId}/secrets`, kv));
|
|
957
|
-
}
|
|
958
|
-
await Promise.all(tasks);
|
|
1167
|
+
await this.client.applyConfig(projectId, { secrets: { shared: projectScoped, services } });
|
|
959
1168
|
}
|
|
960
|
-
/** Delete a secret by key. */
|
|
961
1169
|
async delete(projectId, opts) {
|
|
1170
|
+
const secrets = {};
|
|
962
1171
|
if (opts.serviceId) {
|
|
963
|
-
await this.client.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
1172
|
+
const svc = await this.client.get(`/api/apps/${opts.serviceId}`);
|
|
1173
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1174
|
+
secrets.services = { [svc.name]: { [opts.key]: null } };
|
|
1175
|
+
} else secrets.shared = { [opts.key]: null };
|
|
1176
|
+
await this.client.applyConfig(projectId, { secrets });
|
|
1177
|
+
}
|
|
1178
|
+
refs(projectId, serviceId) {
|
|
1179
|
+
return this.client.get(serviceId ? `/api/apps/${serviceId}/variables:refs` : `/api/projects/${projectId}/variables:refs`);
|
|
967
1180
|
}
|
|
968
1181
|
};
|
|
969
1182
|
|
|
@@ -976,7 +1189,14 @@ var DomainsAPI = class {
|
|
|
976
1189
|
* Show the current domain(s) for a service.
|
|
977
1190
|
* The platform-assigned `.onlizard.com` subdomain is always present.
|
|
978
1191
|
*/
|
|
979
|
-
list(serviceId) {
|
|
1192
|
+
async list(serviceId) {
|
|
1193
|
+
const result = await this.info(serviceId);
|
|
1194
|
+
return [
|
|
1195
|
+
...result.domains.map((domain) => ({ domain, verified: true, live: result.dnsOk?.[domain], cnameTarget: result.cnameTarget })),
|
|
1196
|
+
...result.pending.map((item) => ({ ...item, domain: item.hostname, verified: false }))
|
|
1197
|
+
];
|
|
1198
|
+
}
|
|
1199
|
+
info(serviceId) {
|
|
980
1200
|
return this.client.get(`/api/apps/${serviceId}/domains`);
|
|
981
1201
|
}
|
|
982
1202
|
/**
|
|
@@ -989,15 +1209,23 @@ var DomainsAPI = class {
|
|
|
989
1209
|
* console.log('Add CNAME:', info.cnameTarget)
|
|
990
1210
|
* ```
|
|
991
1211
|
*/
|
|
992
|
-
add(serviceId, domain) {
|
|
993
|
-
|
|
1212
|
+
async add(serviceId, domain, opts = {}) {
|
|
1213
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains`, { hostname: domain, ...opts });
|
|
1214
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
994
1215
|
}
|
|
995
1216
|
/**
|
|
996
1217
|
* Verify that DNS records have propagated and activate the domain.
|
|
997
1218
|
* Call after adding the CNAME or TXT record returned by `add()`.
|
|
998
1219
|
*/
|
|
999
|
-
verify(serviceId, domain) {
|
|
1000
|
-
|
|
1220
|
+
async verify(serviceId, domain) {
|
|
1221
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains/verify`, { hostname: domain });
|
|
1222
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
1223
|
+
}
|
|
1224
|
+
generate(serviceId) {
|
|
1225
|
+
return this.client.post(`/api/apps/${serviceId}/domains`, { generate: true });
|
|
1226
|
+
}
|
|
1227
|
+
delete(serviceId, hostname) {
|
|
1228
|
+
return this.client.delete(`/api/apps/${serviceId}/domains/${segment(hostname)}`);
|
|
1001
1229
|
}
|
|
1002
1230
|
};
|
|
1003
1231
|
|
|
@@ -1006,40 +1234,42 @@ var MetricsAPI = class {
|
|
|
1006
1234
|
constructor(client) {
|
|
1007
1235
|
this.client = client;
|
|
1008
1236
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return this.client.get(`/api/apps/${id}/metrics/cpu?range=${range}`);
|
|
1237
|
+
service(id, range = "1h") {
|
|
1238
|
+
return this.client.get(`/api/apps/${id}/metrics?range=${range}`);
|
|
1012
1239
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
return this.client.get(`/api/apps/${id}/metrics/memory?range=${range}`);
|
|
1240
|
+
addon(projectId, addonId, range = "1h") {
|
|
1241
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/metrics?range=${range}`);
|
|
1016
1242
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return this.client.get(`/api/apps/${id}/metrics/network?range=${range}`);
|
|
1243
|
+
project(id, opts = {}) {
|
|
1244
|
+
return this.client.get(query(`/api/projects/${id}/metrics`, opts));
|
|
1020
1245
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return
|
|
1246
|
+
points(raw, name) {
|
|
1247
|
+
const series = raw.series.find((s) => s.metric === name);
|
|
1248
|
+
return (series?.values ?? []).flatMap((value, i) => series?.available?.[i] === false ? [] : [{ t: raw.timestamps[i], value }]);
|
|
1024
1249
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1250
|
+
async cpu(id, range = "1h") {
|
|
1251
|
+
return this.points(await this.service(id, range), "cpu");
|
|
1252
|
+
}
|
|
1253
|
+
async memory(id, range = "1h") {
|
|
1254
|
+
return this.points(await this.service(id, range), "memory");
|
|
1255
|
+
}
|
|
1256
|
+
async network(id, range = "1h") {
|
|
1257
|
+
const raw = await this.service(id, range);
|
|
1258
|
+
return { rx: this.points(raw, "network_rx"), tx: this.points(raw, "network_tx") };
|
|
1259
|
+
}
|
|
1260
|
+
async disk(id, range = "1h") {
|
|
1261
|
+
const raw = await this.service(id, range);
|
|
1262
|
+
return { read: this.points(raw, "disk_read"), write: this.points(raw, "disk_write") };
|
|
1031
1263
|
}
|
|
1032
|
-
/**
|
|
1033
|
-
* Convenience: fetch all metric types for a service in one call.
|
|
1034
|
-
*/
|
|
1035
1264
|
async all(id, range = "1h") {
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1265
|
+
const raw = await this.service(id, range);
|
|
1266
|
+
return { cpu: this.points(raw, "cpu"), memory: this.points(raw, "memory"), networkRx: this.points(raw, "network_rx"), networkTx: this.points(raw, "network_tx"), diskRead: this.points(raw, "disk_read"), diskWrite: this.points(raw, "disk_write") };
|
|
1267
|
+
}
|
|
1268
|
+
/** Return the backend's project cost row in USD, without guessing missing totals. */
|
|
1269
|
+
async cost(projectId, range = "30d") {
|
|
1270
|
+
const project = await this.client.get(`/api/projects/${projectId}`);
|
|
1271
|
+
const summary = await this.client.get(query("/api/billing/summary", { workspaceId: project.workspaceId, range }));
|
|
1272
|
+
return summary.projects.find((p) => p.projectId === projectId) ?? null;
|
|
1043
1273
|
}
|
|
1044
1274
|
};
|
|
1045
1275
|
|
|
@@ -1124,6 +1354,71 @@ var RegionsAPI = class {
|
|
|
1124
1354
|
}
|
|
1125
1355
|
};
|
|
1126
1356
|
|
|
1357
|
+
// src/cli.ts
|
|
1358
|
+
import { spawn } from "child_process";
|
|
1359
|
+
var LizardCLI = class {
|
|
1360
|
+
constructor(opts = {}) {
|
|
1361
|
+
this.opts = opts;
|
|
1362
|
+
}
|
|
1363
|
+
run(args, opts = {}) {
|
|
1364
|
+
return new Promise((resolve, reject) => {
|
|
1365
|
+
const env = { ...process.env };
|
|
1366
|
+
if (this.opts.apiKey) {
|
|
1367
|
+
env.LIZARD_API_KEY = this.opts.apiKey;
|
|
1368
|
+
env.LIZARD_TOKEN = this.opts.apiKey;
|
|
1369
|
+
}
|
|
1370
|
+
if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
|
|
1371
|
+
const child = spawn(this.opts.executable ?? "lizard", ["--json", ...args], {
|
|
1372
|
+
cwd: this.opts.cwd,
|
|
1373
|
+
env,
|
|
1374
|
+
shell: false,
|
|
1375
|
+
stdio: "pipe",
|
|
1376
|
+
signal: opts.signal,
|
|
1377
|
+
timeout: this.opts.timeoutMs ?? 6e5
|
|
1378
|
+
});
|
|
1379
|
+
child.stdout.setEncoding("utf8");
|
|
1380
|
+
child.stderr.setEncoding("utf8");
|
|
1381
|
+
let stdout = "", stderr = "", bytes = 0;
|
|
1382
|
+
const collect = (stream, data) => {
|
|
1383
|
+
bytes += Buffer.byteLength(data);
|
|
1384
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
1385
|
+
child.kill();
|
|
1386
|
+
reject(new Error("CLI output exceeds 16 MiB"));
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (stream === "stdout") stdout += data;
|
|
1390
|
+
else stderr += data;
|
|
1391
|
+
};
|
|
1392
|
+
child.stdout.on("data", (data) => collect("stdout", data));
|
|
1393
|
+
child.stderr.on("data", (data) => collect("stderr", data));
|
|
1394
|
+
child.on("error", reject);
|
|
1395
|
+
child.on("close", (code, signal) => {
|
|
1396
|
+
if (signal) {
|
|
1397
|
+
reject(new Error(`CLI ended with signal ${signal}`));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
let events = [];
|
|
1401
|
+
try {
|
|
1402
|
+
events = [JSON.parse(stdout)];
|
|
1403
|
+
} catch {
|
|
1404
|
+
events = stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
1405
|
+
try {
|
|
1406
|
+
return [JSON.parse(line)];
|
|
1407
|
+
} catch {
|
|
1408
|
+
return [];
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
resolve({ code: code ?? 1, stdout, stderr, events });
|
|
1413
|
+
});
|
|
1414
|
+
child.stdin.on("error", (error) => {
|
|
1415
|
+
if (error.code !== "EPIPE") reject(error);
|
|
1416
|
+
});
|
|
1417
|
+
child.stdin.end(opts.stdin);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1127
1422
|
// src/platform/billing.ts
|
|
1128
1423
|
var BillingAPI = class {
|
|
1129
1424
|
constructor(client) {
|
|
@@ -1150,6 +1445,46 @@ var BillingAPI = class {
|
|
|
1150
1445
|
live() {
|
|
1151
1446
|
return this.client.get("/api/billing/live");
|
|
1152
1447
|
}
|
|
1448
|
+
paymentMethods() {
|
|
1449
|
+
return this.client.get("/api/billing/payment-methods");
|
|
1450
|
+
}
|
|
1451
|
+
setupPaymentMethod(returnUrl) {
|
|
1452
|
+
return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
|
|
1453
|
+
}
|
|
1454
|
+
removePaymentMethod(id) {
|
|
1455
|
+
return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
|
|
1456
|
+
}
|
|
1457
|
+
purchase(creditCents, opts = {}) {
|
|
1458
|
+
if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
|
|
1459
|
+
return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
|
|
1460
|
+
}
|
|
1461
|
+
autoTopup() {
|
|
1462
|
+
return this.client.get("/api/billing/auto-topup");
|
|
1463
|
+
}
|
|
1464
|
+
setAutoTopup(opts) {
|
|
1465
|
+
return this.client.put("/api/billing/auto-topup", opts);
|
|
1466
|
+
}
|
|
1467
|
+
runAutoTopup() {
|
|
1468
|
+
return this.client.post("/api/billing/auto-topup/run", {});
|
|
1469
|
+
}
|
|
1470
|
+
redeemPromo(code) {
|
|
1471
|
+
return this.client.post("/api/billing/promo/redeem", { code });
|
|
1472
|
+
}
|
|
1473
|
+
x402Quote(creditCents) {
|
|
1474
|
+
return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
|
|
1475
|
+
}
|
|
1476
|
+
paymentStatus(attemptId) {
|
|
1477
|
+
return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
|
|
1478
|
+
}
|
|
1479
|
+
/** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
|
|
1480
|
+
async payX402(creditCents, opts) {
|
|
1481
|
+
if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
|
|
1482
|
+
const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
|
|
1483
|
+
if (opts.requestId) args.push("--request-id", opts.requestId);
|
|
1484
|
+
const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
|
|
1485
|
+
if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
|
|
1486
|
+
return result.events[0];
|
|
1487
|
+
}
|
|
1153
1488
|
};
|
|
1154
1489
|
|
|
1155
1490
|
// src/volume/index.ts
|
|
@@ -1276,6 +1611,8 @@ var Lizard = class {
|
|
|
1276
1611
|
this.secrets = new SecretsAPI(platform);
|
|
1277
1612
|
this.domains = new DomainsAPI(platform);
|
|
1278
1613
|
this.metrics = new MetricsAPI(platform);
|
|
1614
|
+
this.storage = new StorageAPI(platform);
|
|
1615
|
+
this.github = new GitHubAPI(platform);
|
|
1279
1616
|
}
|
|
1280
1617
|
// ── Sandbox convenience methods (backward-compatible) ──────────────────
|
|
1281
1618
|
/** Resolve the client's project reference to a stable project ID (cached). */
|
|
@@ -1368,8 +1705,7 @@ var CodeSandbox = class extends Sandbox {
|
|
|
1368
1705
|
*
|
|
1369
1706
|
* @returns Execution result with stdout, stderr, results, and any error.
|
|
1370
1707
|
*
|
|
1371
|
-
*
|
|
1372
|
-
* default context exists — which cannot happen in normal usage.
|
|
1708
|
+
* Code errors appear in `execution.error`; request failures throw.
|
|
1373
1709
|
*
|
|
1374
1710
|
* @example
|
|
1375
1711
|
* ```ts
|
|
@@ -1519,10 +1855,13 @@ export {
|
|
|
1519
1855
|
AuthenticationError,
|
|
1520
1856
|
BillingAPI,
|
|
1521
1857
|
CodeSandbox,
|
|
1858
|
+
ConfigApplyError,
|
|
1522
1859
|
ConflictError,
|
|
1523
1860
|
DeployHandle,
|
|
1524
1861
|
DomainsAPI,
|
|
1862
|
+
GitHubAPI,
|
|
1525
1863
|
Lizard,
|
|
1864
|
+
LizardCLI,
|
|
1526
1865
|
LizardError,
|
|
1527
1866
|
MetricsAPI,
|
|
1528
1867
|
NotFoundError,
|
|
@@ -1532,6 +1871,7 @@ export {
|
|
|
1532
1871
|
Sandbox,
|
|
1533
1872
|
SecretsAPI,
|
|
1534
1873
|
ServicesAPI,
|
|
1874
|
+
StorageAPI,
|
|
1535
1875
|
TimeoutError,
|
|
1536
1876
|
Volume,
|
|
1537
1877
|
WorkspacesAPI,
|