@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.js
CHANGED
|
@@ -25,10 +25,13 @@ __export(index_exports, {
|
|
|
25
25
|
AuthenticationError: () => AuthenticationError,
|
|
26
26
|
BillingAPI: () => BillingAPI,
|
|
27
27
|
CodeSandbox: () => CodeSandbox,
|
|
28
|
+
ConfigApplyError: () => ConfigApplyError,
|
|
28
29
|
ConflictError: () => ConflictError,
|
|
29
30
|
DeployHandle: () => DeployHandle,
|
|
30
31
|
DomainsAPI: () => DomainsAPI,
|
|
32
|
+
GitHubAPI: () => GitHubAPI,
|
|
31
33
|
Lizard: () => Lizard,
|
|
34
|
+
LizardCLI: () => LizardCLI,
|
|
32
35
|
LizardError: () => LizardError,
|
|
33
36
|
MetricsAPI: () => MetricsAPI,
|
|
34
37
|
NotFoundError: () => NotFoundError,
|
|
@@ -38,6 +41,7 @@ __export(index_exports, {
|
|
|
38
41
|
Sandbox: () => Sandbox,
|
|
39
42
|
SecretsAPI: () => SecretsAPI,
|
|
40
43
|
ServicesAPI: () => ServicesAPI,
|
|
44
|
+
StorageAPI: () => StorageAPI,
|
|
41
45
|
TimeoutError: () => TimeoutError,
|
|
42
46
|
Volume: () => Volume,
|
|
43
47
|
WorkspacesAPI: () => WorkspacesAPI,
|
|
@@ -53,6 +57,7 @@ var ConnectionConfig = class {
|
|
|
53
57
|
this.apiKey = opts?.apiKey ?? process.env.LIZARD_API_KEY ?? "";
|
|
54
58
|
this.apiUrl = opts?.apiUrl ?? process.env.LIZARD_API_URL ?? DEFAULT_API_URL;
|
|
55
59
|
this.timeoutMs = opts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
60
|
+
this.requestTimeoutMs = opts?.requestTimeoutMs ?? 12e4;
|
|
56
61
|
if (!this.apiKey) {
|
|
57
62
|
throw new Error(
|
|
58
63
|
"Lizard API key is required. Set LIZARD_API_KEY env var or pass apiKey in options."
|
|
@@ -74,6 +79,13 @@ var LizardError = class extends Error {
|
|
|
74
79
|
this.name = "LizardError";
|
|
75
80
|
}
|
|
76
81
|
};
|
|
82
|
+
var ConfigApplyError = class extends LizardError {
|
|
83
|
+
constructor(result) {
|
|
84
|
+
super("Config was saved, but one or more side effects failed; inspect result before retrying");
|
|
85
|
+
this.result = result;
|
|
86
|
+
this.name = "ConfigApplyError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
77
89
|
var AuthenticationError = class extends LizardError {
|
|
78
90
|
constructor(message = "Invalid or missing API key") {
|
|
79
91
|
super(message);
|
|
@@ -113,19 +125,358 @@ async function handleApiError(res) {
|
|
|
113
125
|
throw new LizardError(`API error ${res.status}: ${message}`);
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
// src/platform/client.ts
|
|
129
|
+
var segment = (value) => encodeURIComponent(value);
|
|
130
|
+
function query(path, values) {
|
|
131
|
+
const qs = new URLSearchParams();
|
|
132
|
+
for (const [key, value] of Object.entries(values)) if (value !== void 0) qs.set(key, String(value));
|
|
133
|
+
return qs.size ? `${path}?${qs}` : path;
|
|
134
|
+
}
|
|
135
|
+
var PlatformClient = class {
|
|
136
|
+
constructor(opts) {
|
|
137
|
+
this.config = new ConnectionConfig(opts);
|
|
138
|
+
}
|
|
139
|
+
async json(res) {
|
|
140
|
+
if (!res.ok) await handleApiError(res);
|
|
141
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
return text ? JSON.parse(text) : void 0;
|
|
144
|
+
}
|
|
145
|
+
request(method, path, body) {
|
|
146
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
147
|
+
method,
|
|
148
|
+
headers: this.config.headers,
|
|
149
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
150
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
151
|
+
}).then((res) => this.json(res));
|
|
152
|
+
}
|
|
153
|
+
get(path) {
|
|
154
|
+
return this.request("GET", path);
|
|
155
|
+
}
|
|
156
|
+
post(path, body) {
|
|
157
|
+
return this.request("POST", path, body);
|
|
158
|
+
}
|
|
159
|
+
put(path, body) {
|
|
160
|
+
return this.request("PUT", path, body);
|
|
161
|
+
}
|
|
162
|
+
patch(path, body) {
|
|
163
|
+
return this.request("PATCH", path, body);
|
|
164
|
+
}
|
|
165
|
+
delete(path, body) {
|
|
166
|
+
return this.request("DELETE", path, body);
|
|
167
|
+
}
|
|
168
|
+
async applyConfig(projectId, body) {
|
|
169
|
+
const result = await this.post(`/api/projects/${projectId}/config:apply`, body);
|
|
170
|
+
if (result?.sideEffectFailures?.length) throw new ConfigApplyError(result);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
sendBytes(method, path, body, contentType = "application/octet-stream") {
|
|
174
|
+
const payload = typeof body === "string" || body instanceof Blob ? body : new Blob([new Uint8Array(body)]);
|
|
175
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
176
|
+
method,
|
|
177
|
+
headers: { ...this.config.headers, "Content-Type": contentType },
|
|
178
|
+
body: payload,
|
|
179
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
180
|
+
}).then((res) => this.json(res));
|
|
181
|
+
}
|
|
182
|
+
postForm(path, form) {
|
|
183
|
+
return fetch(`${this.config.apiUrl}${path}`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "X-API-Key": this.config.apiKey },
|
|
186
|
+
body: form,
|
|
187
|
+
signal: AbortSignal.timeout(this.config.requestTimeoutMs)
|
|
188
|
+
}).then((res) => this.json(res));
|
|
189
|
+
}
|
|
190
|
+
/** Preserves named events, multiline data and UTF-8 split across chunks. */
|
|
191
|
+
async *events(path, opts = {}) {
|
|
192
|
+
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
193
|
+
method: opts.method ?? "GET",
|
|
194
|
+
headers: this.config.headers,
|
|
195
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
196
|
+
signal: opts.signal
|
|
197
|
+
});
|
|
198
|
+
if (!res.ok) await handleApiError(res);
|
|
199
|
+
if (!res.body) return;
|
|
200
|
+
const reader = res.body.getReader(), decoder = new TextDecoder();
|
|
201
|
+
let buffer = "", event = "message", id, data = [];
|
|
202
|
+
const line = (text) => {
|
|
203
|
+
if (text === "") {
|
|
204
|
+
const result = data.length ? { event, data: data.join("\n"), id } : void 0;
|
|
205
|
+
event = "message";
|
|
206
|
+
data = [];
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
if (text.startsWith(":")) return;
|
|
210
|
+
const colon = text.indexOf(":");
|
|
211
|
+
const field = colon < 0 ? text : text.slice(0, colon);
|
|
212
|
+
const value = colon < 0 ? "" : text.slice(colon + 1).replace(/^ /, "");
|
|
213
|
+
if (field === "data") data.push(value);
|
|
214
|
+
if (field === "event") event = value;
|
|
215
|
+
if (field === "id" && !value.includes("\0")) id = value;
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
while (true) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
221
|
+
let end;
|
|
222
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
223
|
+
const result = line(buffer.slice(0, end).replace(/\r$/, ""));
|
|
224
|
+
buffer = buffer.slice(end + 1);
|
|
225
|
+
if (result) yield result;
|
|
226
|
+
}
|
|
227
|
+
if (done) break;
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
await reader.cancel();
|
|
231
|
+
reader.releaseLock();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async *streamSse(path) {
|
|
235
|
+
for await (const event of this.events(path)) yield event.data;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/platform/storage.ts
|
|
240
|
+
var StorageAPI = class {
|
|
241
|
+
constructor(client) {
|
|
242
|
+
this.client = client;
|
|
243
|
+
}
|
|
244
|
+
list(projectId, addonId, opts = {}) {
|
|
245
|
+
return this.client.get(query(`/api/projects/${projectId}/addons/${addonId}/s3/buckets/${segment(opts.bucket ?? "default")}/objects`, { prefix: opts.prefix }));
|
|
246
|
+
}
|
|
247
|
+
upload(projectId, addonId, opts) {
|
|
248
|
+
const key = opts.key.split("/").map(segment).join("/");
|
|
249
|
+
return this.client.sendBytes("PUT", `/api/projects/${projectId}/addons/${addonId}/s3/objects/${segment(opts.bucket ?? "default")}/${key}`, opts.source, opts.contentType);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/platform/services.ts
|
|
254
|
+
var DeployHandle = class {
|
|
255
|
+
constructor(client, serviceId, buildId) {
|
|
256
|
+
this._client = client;
|
|
257
|
+
this._serviceId = serviceId;
|
|
258
|
+
this._buildId = buildId;
|
|
259
|
+
}
|
|
260
|
+
get serviceId() {
|
|
261
|
+
return this._serviceId;
|
|
262
|
+
}
|
|
263
|
+
/** Stream build log messages until the server closes the build stream. */
|
|
264
|
+
async *logs() {
|
|
265
|
+
let buildId = this._buildId;
|
|
266
|
+
if (!buildId) {
|
|
267
|
+
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
268
|
+
buildId = svc.builds?.[0]?.id;
|
|
269
|
+
}
|
|
270
|
+
if (!buildId) throw new LizardError("No build found for this deploy");
|
|
271
|
+
for await (const event of this._client.events(`/api/builds/${buildId}/logs`)) {
|
|
272
|
+
if (event.event === "error") throw new LizardError(event.data);
|
|
273
|
+
if (event.event === "done") return;
|
|
274
|
+
let data;
|
|
275
|
+
try {
|
|
276
|
+
data = JSON.parse(event.data);
|
|
277
|
+
} catch {
|
|
278
|
+
data = event.data;
|
|
279
|
+
}
|
|
280
|
+
yield typeof data === "string" ? data : event.data;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Poll until the deploy reaches a terminal state.
|
|
285
|
+
* @returns `{ url, status }` on success; throws `LizardError` on failure.
|
|
286
|
+
*/
|
|
287
|
+
async wait(opts) {
|
|
288
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 10 * 6e4);
|
|
289
|
+
const pollMs = opts?.pollMs ?? 3e3;
|
|
290
|
+
while (Date.now() < deadline) {
|
|
291
|
+
let buildDone = true;
|
|
292
|
+
if (this._buildId) {
|
|
293
|
+
const build = await this._client.get(`/api/builds/${this._buildId}`);
|
|
294
|
+
if (build.status === "failed" || build.status === "cancelled") throw new LizardError(`Deploy ${build.status}`);
|
|
295
|
+
buildDone = build.status === "done";
|
|
296
|
+
}
|
|
297
|
+
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
298
|
+
if (svc.deployStatus === "idle") {
|
|
299
|
+
if (buildDone && svc.status === "running") return { url: svc.domain ? `https://${svc.domain}` : null, status: "running" };
|
|
300
|
+
if (svc.status === "crashed") throw new LizardError(`Service crashed after deploy`);
|
|
301
|
+
}
|
|
302
|
+
if (svc.deployStatus === "failed") throw new LizardError(`Deploy failed`);
|
|
303
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
304
|
+
}
|
|
305
|
+
throw new TimeoutError(`Deploy did not complete within ${opts?.timeoutMs ?? 6e5}ms`);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var ServicesAPI = class {
|
|
309
|
+
constructor(client) {
|
|
310
|
+
this.client = client;
|
|
311
|
+
}
|
|
312
|
+
/** List all services in a project. */
|
|
313
|
+
list(opts) {
|
|
314
|
+
return this.client.get(`/api/projects/${opts.projectId}/apps`);
|
|
315
|
+
}
|
|
316
|
+
/** Get a service by ID. */
|
|
317
|
+
get(id) {
|
|
318
|
+
return this.client.get(`/api/apps/${id}`);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Create a service and start a deploy from a git repo.
|
|
322
|
+
* Returns a DeployHandle to track progress.
|
|
323
|
+
*/
|
|
324
|
+
async deploy(opts) {
|
|
325
|
+
const body = {
|
|
326
|
+
name: opts.name,
|
|
327
|
+
sourceType: opts.sourceType ?? "github",
|
|
328
|
+
repoUrl: opts.repoUrl,
|
|
329
|
+
branch: opts.branch ?? "main",
|
|
330
|
+
startCommand: opts.startCommand,
|
|
331
|
+
buildCommand: opts.buildCommand,
|
|
332
|
+
preDeployCommand: opts.preDeployCommand,
|
|
333
|
+
dockerfilePath: opts.dockerfilePath,
|
|
334
|
+
context: opts.rootDirectory,
|
|
335
|
+
envVars: opts.envVars,
|
|
336
|
+
skipInitialDeploy: opts.skipInitialDeploy,
|
|
337
|
+
cpuLimit: opts.cpuLimit,
|
|
338
|
+
memoryLimit: opts.memoryLimit,
|
|
339
|
+
containerPort: opts.port ?? opts.containerPort,
|
|
340
|
+
region: opts.region
|
|
341
|
+
};
|
|
342
|
+
const svc = await this.client.post(`/api/projects/${opts.projectId}/apps`, body);
|
|
343
|
+
const handle = new DeployHandle(this.client, svc.id, svc.buildId);
|
|
344
|
+
if (opts.waitForDeploy) await handle.wait();
|
|
345
|
+
return handle;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Upload a tarball and deploy it.
|
|
349
|
+
*
|
|
350
|
+
* @param opts.source - A `Buffer`, `Blob`, or `Uint8Array` of a `.tar.gz` file.
|
|
351
|
+
*/
|
|
352
|
+
async upload(opts) {
|
|
353
|
+
if (!opts.name && !opts.serviceId) throw new LizardError("name or serviceId is required");
|
|
354
|
+
const path = query(`/api/projects/${opts.projectId}/apps/upload`, {
|
|
355
|
+
name: opts.name,
|
|
356
|
+
appId: opts.serviceId,
|
|
357
|
+
startCommand: opts.startCommand,
|
|
358
|
+
buildCommand: opts.buildCommand,
|
|
359
|
+
preDeployCommand: opts.preDeployCommand,
|
|
360
|
+
port: opts.port,
|
|
361
|
+
region: opts.region
|
|
362
|
+
});
|
|
363
|
+
const result = await this.client.sendBytes("POST", path, opts.source);
|
|
364
|
+
return new DeployHandle(this.client, result.id, result.buildId);
|
|
365
|
+
}
|
|
366
|
+
/** Trigger a redeploy (rebuild from current source). */
|
|
367
|
+
async redeploy(id) {
|
|
368
|
+
const build = await this.client.post(`/api/apps/${id}/redeploy`, {});
|
|
369
|
+
return new DeployHandle(this.client, id, build.id);
|
|
370
|
+
}
|
|
371
|
+
/** Restart a service without rebuilding. */
|
|
372
|
+
restart(id) {
|
|
373
|
+
return this.client.post(`/api/apps/${id}/restart`, {});
|
|
374
|
+
}
|
|
375
|
+
/** Scale a service. */
|
|
376
|
+
async scale(id, opts) {
|
|
377
|
+
if (opts.storageMi !== void 0) throw new LizardError("Storage scaling is only supported for addons");
|
|
378
|
+
if (opts.replicas !== void 0) await this.client.patch(`/api/apps/${id}/scale`, { replicas: opts.replicas });
|
|
379
|
+
if (opts.cpuMillis !== void 0 || opts.memoryMi !== void 0) {
|
|
380
|
+
await this.update(id, {
|
|
381
|
+
...opts.cpuMillis !== void 0 ? { cpuLimit: `${opts.cpuMillis}m` } : {},
|
|
382
|
+
...opts.memoryMi !== void 0 ? { memoryLimit: `${opts.memoryMi}Mi` } : {}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get recent log lines. For a live tail, use the WebSocket API instead.
|
|
388
|
+
*
|
|
389
|
+
* @param opts.limit - Max lines to return (default 200, max 1000).
|
|
390
|
+
*/
|
|
391
|
+
async logs(id, opts) {
|
|
392
|
+
const svc = await this.get(id);
|
|
393
|
+
const result = await this.client.get(query(`/api/projects/${svc.projectId}/logs`, { service: svc.name, limit: opts?.limit ?? 200, since: opts?.since }));
|
|
394
|
+
return Array.isArray(result) ? result : result.logs ?? [];
|
|
395
|
+
}
|
|
396
|
+
/** Execute a command; collect SSE output and require an exit event. */
|
|
397
|
+
async exec(id, cmd, opts) {
|
|
398
|
+
const result = { stdout: "", stderr: "", exitCode: -1 };
|
|
399
|
+
for await (const event of this.client.events(`/api/apps/${id}/exec`, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
body: { cmd },
|
|
402
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 3e5)
|
|
403
|
+
})) {
|
|
404
|
+
if (event.event === "error") throw new LizardError(event.data);
|
|
405
|
+
const data = JSON.parse(event.data);
|
|
406
|
+
if (data.stream === "stdout" || data.stream === "stderr") result[data.stream] += (data.line ?? "") + "\n";
|
|
407
|
+
if (event.event === "exit" && typeof data.exitCode === "number") result.exitCode = data.exitCode;
|
|
408
|
+
}
|
|
409
|
+
if (result.exitCode === -1) throw new LizardError("Exec stream ended without exit status");
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
async update(id, opts, controls = {}) {
|
|
413
|
+
const svc = await this.get(id);
|
|
414
|
+
let revision = controls.revision;
|
|
415
|
+
if (!controls.force && revision === void 0) {
|
|
416
|
+
const project = await this.client.get(`/api/projects/${svc.projectId}`);
|
|
417
|
+
revision = project.configRevision;
|
|
418
|
+
}
|
|
419
|
+
return this.client.applyConfig(svc.projectId, {
|
|
420
|
+
...controls.force ? {} : { revision },
|
|
421
|
+
services: [{ name: svc.name, ...opts, id }]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
events(id, buildId) {
|
|
425
|
+
return this.client.get(query(`/api/apps/${id}/deploy-events`, { buildId }));
|
|
426
|
+
}
|
|
427
|
+
pods(id) {
|
|
428
|
+
return this.client.get(`/api/apps/${id}/pod-status`);
|
|
429
|
+
}
|
|
430
|
+
history(id, opts = {}) {
|
|
431
|
+
return this.client.get(query(`/api/apps/${id}/logs/history`, opts));
|
|
432
|
+
}
|
|
433
|
+
streamLogs(id, signal) {
|
|
434
|
+
return this.client.events(`/api/apps/${id}/logs`, { signal });
|
|
435
|
+
}
|
|
436
|
+
buildLogs(buildId, signal) {
|
|
437
|
+
return this.client.events(`/api/builds/${buildId}/logs`, { signal });
|
|
438
|
+
}
|
|
439
|
+
/** Delete a service. */
|
|
440
|
+
delete(id) {
|
|
441
|
+
return this.client.delete(`/api/apps/${id}`);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/platform/github.ts
|
|
446
|
+
var GitHubAPI = class {
|
|
447
|
+
constructor(client) {
|
|
448
|
+
this.client = client;
|
|
449
|
+
}
|
|
450
|
+
status() {
|
|
451
|
+
return this.client.get("/api/github/status");
|
|
452
|
+
}
|
|
453
|
+
/** Complete the GitHub App installation in a browser using the caller's account. */
|
|
454
|
+
installUrl() {
|
|
455
|
+
return `${this.client.config.apiUrl}/api/auth/github/install`;
|
|
456
|
+
}
|
|
457
|
+
async checkout(serviceId, branch) {
|
|
458
|
+
const services = new ServicesAPI(this.client);
|
|
459
|
+
await services.update(serviceId, { branch });
|
|
460
|
+
return services.redeploy(serviceId);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
116
464
|
// src/project.ts
|
|
465
|
+
var import_node_crypto = require("crypto");
|
|
117
466
|
var projectIdCache = /* @__PURE__ */ new Map();
|
|
118
467
|
async function resolveProjectId(ref, config) {
|
|
119
|
-
const cacheKey = `${config.apiUrl}\0${ref}`;
|
|
468
|
+
const cacheKey = `${config.apiUrl}\0${(0, import_node_crypto.createHash)("sha256").update(config.apiKey).digest("hex")}\0${ref}`;
|
|
120
469
|
const cached = projectIdCache.get(cacheKey);
|
|
121
470
|
if (cached) return cached;
|
|
122
471
|
const res = await fetch(`${config.apiUrl}/api/projects`, { headers: config.headers });
|
|
123
472
|
if (!res.ok) await handleApiError(res);
|
|
124
473
|
const projects = await res.json();
|
|
125
474
|
const lower = ref.toLowerCase();
|
|
126
|
-
const
|
|
475
|
+
const matches = projects.filter(
|
|
127
476
|
(p) => p.id.toLowerCase() === lower || p.slug?.toLowerCase() === lower || p.name?.toLowerCase() === lower
|
|
128
477
|
);
|
|
478
|
+
if (matches.length > 1) throw new LizardError("Project reference is ambiguous; pass the project ID");
|
|
479
|
+
const match = matches[0];
|
|
129
480
|
if (!match) {
|
|
130
481
|
const available = projects.map((p) => p.slug || p.id).join(", ") || "(none)";
|
|
131
482
|
throw new LizardError(
|
|
@@ -150,12 +501,12 @@ var Process = class {
|
|
|
150
501
|
this.config = config;
|
|
151
502
|
}
|
|
152
503
|
/**
|
|
153
|
-
* Execute a command inside the
|
|
504
|
+
* Execute a command inside the sandbox and wait for it to complete.
|
|
154
505
|
*
|
|
155
506
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
156
507
|
* stdout, stderr, and the exit code when it finishes.
|
|
157
508
|
*
|
|
158
|
-
* @param cmd Shell command to run inside the
|
|
509
|
+
* @param cmd Shell command to run inside the sandbox.
|
|
159
510
|
* @param opts Optional execution options — environment variables, working
|
|
160
511
|
* directory, user, and timeout.
|
|
161
512
|
*
|
|
@@ -287,22 +638,23 @@ var Fs = class {
|
|
|
287
638
|
this.config = config;
|
|
288
639
|
}
|
|
289
640
|
/**
|
|
290
|
-
* Write a file into the
|
|
641
|
+
* Write a file into the sandbox filesystem.
|
|
291
642
|
*
|
|
292
643
|
* Creates parent directories automatically if they don't exist.
|
|
644
|
+
* Byte input must contain valid UTF-8; arbitrary binary uploads are unsupported.
|
|
293
645
|
*
|
|
294
646
|
* @example
|
|
295
647
|
* ```ts
|
|
296
648
|
* await sandbox.fs.write('/app/index.js', 'console.log("hello")')
|
|
297
649
|
* ```
|
|
298
650
|
*
|
|
299
|
-
* @example Write
|
|
651
|
+
* @example Write UTF-8 bytes:
|
|
300
652
|
* ```ts
|
|
301
|
-
* await sandbox.fs.write('/app/
|
|
653
|
+
* await sandbox.fs.write('/app/notes.txt', new TextEncoder().encode('hello'))
|
|
302
654
|
* ```
|
|
303
655
|
*/
|
|
304
656
|
async write(path, data, opts) {
|
|
305
|
-
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
657
|
+
const content = typeof data === "string" ? data : new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
306
658
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
307
659
|
method: "POST",
|
|
308
660
|
headers: this.config.headers,
|
|
@@ -311,7 +663,7 @@ var Fs = class {
|
|
|
311
663
|
if (!res.ok) await handleApiError(res);
|
|
312
664
|
}
|
|
313
665
|
/**
|
|
314
|
-
* Read a file from the
|
|
666
|
+
* Read a file from the sandbox filesystem.
|
|
315
667
|
*
|
|
316
668
|
* @returns The file contents as a UTF-8 string.
|
|
317
669
|
*
|
|
@@ -328,8 +680,17 @@ var Fs = class {
|
|
|
328
680
|
if (!res.ok) await handleApiError(res);
|
|
329
681
|
return res.text();
|
|
330
682
|
}
|
|
683
|
+
/** Read a file as bytes. */
|
|
684
|
+
async readBytes(path, opts) {
|
|
685
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`);
|
|
686
|
+
url.searchParams.set("path", path);
|
|
687
|
+
if (opts?.user) url.searchParams.set("user", opts.user);
|
|
688
|
+
const res = await fetch(url, { headers: this.config.headers });
|
|
689
|
+
if (!res.ok) await handleApiError(res);
|
|
690
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
691
|
+
}
|
|
331
692
|
/**
|
|
332
|
-
* List files and directories at the given path inside the
|
|
693
|
+
* List files and directories at the given path inside the sandbox.
|
|
333
694
|
*
|
|
334
695
|
* @example
|
|
335
696
|
* ```ts
|
|
@@ -345,7 +706,7 @@ var Fs = class {
|
|
|
345
706
|
return res.json();
|
|
346
707
|
}
|
|
347
708
|
/**
|
|
348
|
-
* Remove a file or directory from the
|
|
709
|
+
* Remove a file or directory from the sandbox filesystem.
|
|
349
710
|
*/
|
|
350
711
|
async remove(path, opts) {
|
|
351
712
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
@@ -356,7 +717,7 @@ var Fs = class {
|
|
|
356
717
|
if (!res.ok) await handleApiError(res);
|
|
357
718
|
}
|
|
358
719
|
/**
|
|
359
|
-
* Create a directory (and any missing parents) inside the
|
|
720
|
+
* Create a directory (and any missing parents) inside the sandbox.
|
|
360
721
|
*/
|
|
361
722
|
async makeDir(path, opts) {
|
|
362
723
|
await this.execInternal(`mkdir -p ${JSON.stringify(path)}`, opts?.user);
|
|
@@ -527,7 +888,8 @@ var SandboxClient = class {
|
|
|
527
888
|
}
|
|
528
889
|
static async listSandboxes(opts) {
|
|
529
890
|
const config = new ConnectionConfig(opts);
|
|
530
|
-
const
|
|
891
|
+
const path = opts?.projectId ? `/api/projects/${encodeURIComponent(opts.projectId)}/sandboxes` : "/api/sandboxes";
|
|
892
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
531
893
|
headers: config.headers
|
|
532
894
|
});
|
|
533
895
|
if (!res.ok) await handleApiError(res);
|
|
@@ -583,11 +945,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
583
945
|
* Verifies the sandbox exists and is reachable, then returns a handle to it.
|
|
584
946
|
* Throws `NotFoundError` if it has been killed or has expired.
|
|
585
947
|
*
|
|
586
|
-
*
|
|
587
|
-
* reconnecting to might be paused. Sandboxes are pods now and pause/resume is a
|
|
588
|
-
* `501` on every one of them, so that call turned every `connect()` into an
|
|
589
|
-
* error against a perfectly healthy sandbox. Connecting does not need to change
|
|
590
|
-
* a sandbox's state, so it no longer tries to.
|
|
948
|
+
* Connecting reads sandbox metadata without changing its state.
|
|
591
949
|
*
|
|
592
950
|
* @example
|
|
593
951
|
* ```ts
|
|
@@ -612,29 +970,25 @@ var Sandbox = class extends SandboxClient {
|
|
|
612
970
|
/**
|
|
613
971
|
* Kill the sandbox and release its resources immediately.
|
|
614
972
|
*
|
|
615
|
-
* @returns `true` if the
|
|
973
|
+
* @returns `true` if the sandbox was terminated, `false` if it was already gone.
|
|
616
974
|
*/
|
|
617
975
|
async kill(opts) {
|
|
618
976
|
return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
619
977
|
}
|
|
620
978
|
/**
|
|
621
|
-
*
|
|
979
|
+
* Request sandbox pause. The Kubernetes backend returns HTTP 501.
|
|
622
980
|
*
|
|
623
|
-
* @deprecated
|
|
624
|
-
*
|
|
625
|
-
* checkpoint of the pod, which is not built. To park work across a gap, put it on a
|
|
626
|
-
* {@link Volume} and create a fresh sandbox on that volume later; the volume is the
|
|
627
|
-
* part that is meant to outlive a sandbox.
|
|
981
|
+
* @deprecated Unsupported on Kubernetes. Save files to a {@link Volume}
|
|
982
|
+
* and attach it to a new sandbox instead.
|
|
628
983
|
*/
|
|
629
984
|
async pause(opts) {
|
|
630
985
|
return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
631
986
|
}
|
|
632
987
|
/**
|
|
633
|
-
*
|
|
988
|
+
* Request sandbox resume. The Kubernetes backend returns HTTP 501.
|
|
634
989
|
*
|
|
635
|
-
* @deprecated
|
|
636
|
-
*
|
|
637
|
-
* calls this, so reconnecting to a running sandbox works without it.
|
|
990
|
+
* @deprecated Unsupported on Kubernetes. Use {@link Sandbox.connect} to
|
|
991
|
+
* reconnect to a running sandbox.
|
|
638
992
|
*/
|
|
639
993
|
async resume(opts) {
|
|
640
994
|
return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
@@ -654,102 +1008,53 @@ var Sandbox = class extends SandboxClient {
|
|
|
654
1008
|
return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
|
|
655
1009
|
}
|
|
656
1010
|
/**
|
|
657
|
-
*
|
|
1011
|
+
* Register a public HTTPS route and return its hostname without a scheme.
|
|
658
1012
|
*
|
|
659
|
-
* Useful for accessing HTTP servers started inside the
|
|
1013
|
+
* Useful for accessing HTTP servers started inside the sandbox from your
|
|
660
1014
|
* agent or tests without additional tunneling.
|
|
661
1015
|
*
|
|
662
1016
|
* @example
|
|
663
1017
|
* ```ts
|
|
664
|
-
*
|
|
665
|
-
* const
|
|
666
|
-
*
|
|
1018
|
+
* // Start an HTTP server on port 3000 before exposing it.
|
|
1019
|
+
* const hostname = await sandbox.getHost(3000)
|
|
1020
|
+
* console.log(`https://${hostname}`)
|
|
667
1021
|
* ```
|
|
668
1022
|
*/
|
|
669
1023
|
async getHost(port, opts) {
|
|
670
1024
|
const { hostname } = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
|
|
671
1025
|
return hostname;
|
|
672
1026
|
}
|
|
673
|
-
|
|
674
|
-
|
|
1027
|
+
/** Fork is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
1028
|
+
fork(opts = {}) {
|
|
1029
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/fork`, { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 });
|
|
675
1030
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// src/platform/client.ts
|
|
681
|
-
var PlatformClient = class {
|
|
682
|
-
constructor(opts) {
|
|
683
|
-
this.config = new ConnectionConfig(opts);
|
|
1031
|
+
/** Snapshot creation is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
1032
|
+
snapshot(name) {
|
|
1033
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name });
|
|
684
1034
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
headers: this.config.headers
|
|
688
|
-
});
|
|
689
|
-
if (!res.ok) await handleApiError(res);
|
|
690
|
-
return res.json();
|
|
1035
|
+
unexpose(port) {
|
|
1036
|
+
return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
|
|
691
1037
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
method: "POST",
|
|
695
|
-
headers: this.config.headers,
|
|
696
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
697
|
-
});
|
|
698
|
-
if (!res.ok) await handleApiError(res);
|
|
699
|
-
return res.json();
|
|
1038
|
+
logs(opts = {}) {
|
|
1039
|
+
return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
|
|
700
1040
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
method: "PATCH",
|
|
704
|
-
headers: this.config.headers,
|
|
705
|
-
body: JSON.stringify(body)
|
|
706
|
-
});
|
|
707
|
-
if (!res.ok) await handleApiError(res);
|
|
708
|
-
return res.json();
|
|
1041
|
+
static snapshots(projectId, opts) {
|
|
1042
|
+
return new PlatformClient(opts ?? {}).get(`/api/projects/${projectId}/snapshots`);
|
|
709
1043
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
715
|
-
});
|
|
716
|
-
if (!res.ok) await handleApiError(res);
|
|
717
|
-
if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
|
|
718
|
-
return res.json();
|
|
1044
|
+
/** Snapshot restore is unsupported on Kubernetes; the backend returns HTTP 501. */
|
|
1045
|
+
static async restore(snapshotId, opts) {
|
|
1046
|
+
const result = await new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/fork`, { timeoutMs: opts?.timeoutMs ?? 0 });
|
|
1047
|
+
return new this({ ...opts, sandboxId: result.sandboxId ?? result.id });
|
|
719
1048
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
723
|
-
method: "POST",
|
|
724
|
-
headers,
|
|
725
|
-
body: form
|
|
726
|
-
});
|
|
727
|
-
if (!res.ok) await handleApiError(res);
|
|
728
|
-
return res.json();
|
|
1049
|
+
static deleteSnapshot(snapshotId, opts) {
|
|
1050
|
+
return new PlatformClient(opts ?? {}).delete(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
729
1051
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const res = await fetch(`${this.config.apiUrl}${path}`, {
|
|
733
|
-
headers: this.config.headers
|
|
734
|
-
});
|
|
735
|
-
if (!res.ok) await handleApiError(res);
|
|
736
|
-
if (!res.body) return;
|
|
737
|
-
const reader = res.body.getReader();
|
|
738
|
-
const decoder = new TextDecoder();
|
|
739
|
-
let buf = "";
|
|
740
|
-
while (true) {
|
|
741
|
-
const { done, value } = await reader.read();
|
|
742
|
-
if (done) break;
|
|
743
|
-
buf += decoder.decode(value, { stream: true });
|
|
744
|
-
const events = buf.split("\n\n");
|
|
745
|
-
buf = events.pop();
|
|
746
|
-
for (const evt of events) {
|
|
747
|
-
const dataLine = evt.split("\n").find((l) => l.startsWith("data: "));
|
|
748
|
-
if (dataLine) yield dataLine.slice(6);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
1052
|
+
resolveOpts(opts) {
|
|
1053
|
+
return { ...this.connectionConfig, ...opts };
|
|
751
1054
|
}
|
|
752
1055
|
};
|
|
1056
|
+
Sandbox.defaultTemplate = "base";
|
|
1057
|
+
Sandbox.defaultTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
753
1058
|
|
|
754
1059
|
// src/platform/projects.ts
|
|
755
1060
|
var ProjectsAPI = class {
|
|
@@ -777,134 +1082,20 @@ var ProjectsAPI = class {
|
|
|
777
1082
|
delete(id) {
|
|
778
1083
|
return this.client.delete(`/api/projects/${id}`);
|
|
779
1084
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
// src/platform/services.ts
|
|
783
|
-
var DeployHandle = class {
|
|
784
|
-
constructor(client, serviceId, buildId) {
|
|
785
|
-
this._client = client;
|
|
786
|
-
this._serviceId = serviceId;
|
|
787
|
-
this._buildId = buildId;
|
|
788
|
-
}
|
|
789
|
-
get serviceId() {
|
|
790
|
-
return this._serviceId;
|
|
1085
|
+
apply(id, config) {
|
|
1086
|
+
return this.client.applyConfig(id, config);
|
|
791
1087
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const path = `/api/apps/${this._serviceId}/logs?limit=1000`;
|
|
795
|
-
const res = await fetch(`${this._client.config.apiUrl}${path}`, {
|
|
796
|
-
headers: this._client.config.headers
|
|
797
|
-
});
|
|
798
|
-
if (!res.ok) return;
|
|
799
|
-
const body = await res.json();
|
|
800
|
-
const lines = Array.isArray(body) ? body : body.logs ?? [];
|
|
801
|
-
for (const l of lines) yield typeof l === "string" ? l : JSON.stringify(l);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Poll until the deploy reaches a terminal state.
|
|
805
|
-
* @returns `{ url, status }` on success; throws `LizardError` on failure.
|
|
806
|
-
*/
|
|
807
|
-
async wait(opts) {
|
|
808
|
-
const deadline = Date.now() + (opts?.timeoutMs ?? 10 * 6e4);
|
|
809
|
-
const pollMs = opts?.pollMs ?? 3e3;
|
|
810
|
-
while (Date.now() < deadline) {
|
|
811
|
-
const svc = await this._client.get(`/api/apps/${this._serviceId}`);
|
|
812
|
-
if (svc.deployStatus === "idle") {
|
|
813
|
-
if (svc.status === "running") return { url: svc.domain ? `https://${svc.domain}` : null, status: "running" };
|
|
814
|
-
if (svc.status === "crashed") throw new LizardError(`Service crashed after deploy`);
|
|
815
|
-
}
|
|
816
|
-
if (svc.deployStatus === "failed") throw new LizardError(`Deploy failed`);
|
|
817
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
818
|
-
}
|
|
819
|
-
throw new TimeoutError(`Deploy did not complete within ${opts?.timeoutMs ?? 6e5}ms`);
|
|
1088
|
+
services(id) {
|
|
1089
|
+
return this.client.get(`/api/projects/${id}/services`);
|
|
820
1090
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
constructor(client) {
|
|
824
|
-
this.client = client;
|
|
1091
|
+
volumeLimits(id) {
|
|
1092
|
+
return this.client.get(`/api/projects/${id}/volume-limits`);
|
|
825
1093
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return this.client.get(`/api/projects/${opts.projectId}/apps`);
|
|
1094
|
+
logs(id, opts = {}) {
|
|
1095
|
+
return this.client.get(query(`/api/projects/${id}/logs`, opts));
|
|
829
1096
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return this.client.get(`/api/apps/${id}`);
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Create a service and start a deploy from a git repo.
|
|
836
|
-
* Returns a DeployHandle to track progress.
|
|
837
|
-
*/
|
|
838
|
-
async deploy(opts) {
|
|
839
|
-
const body = {
|
|
840
|
-
name: opts.name,
|
|
841
|
-
sourceType: opts.sourceType ?? "github",
|
|
842
|
-
repoUrl: opts.repoUrl,
|
|
843
|
-
branch: opts.branch ?? "main",
|
|
844
|
-
startCommand: opts.startCommand,
|
|
845
|
-
buildCommand: opts.buildCommand,
|
|
846
|
-
containerPort: opts.port ?? opts.containerPort,
|
|
847
|
-
region: opts.region
|
|
848
|
-
};
|
|
849
|
-
const svc = await this.client.post(`/api/projects/${opts.projectId}/apps`, body);
|
|
850
|
-
return new DeployHandle(this.client, svc.id);
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Upload a tarball and deploy it.
|
|
854
|
-
*
|
|
855
|
-
* @param opts.source - A `Buffer`, `Blob`, or `Uint8Array` of a `.tar.gz` file.
|
|
856
|
-
*/
|
|
857
|
-
async upload(opts) {
|
|
858
|
-
const form = new FormData();
|
|
859
|
-
const blob = opts.source instanceof Blob ? opts.source : new Blob([opts.source], { type: "application/gzip" });
|
|
860
|
-
form.append("file", blob, "source.tar.gz");
|
|
861
|
-
form.append("name", opts.name);
|
|
862
|
-
if (opts.startCommand) form.append("startCommand", opts.startCommand);
|
|
863
|
-
if (opts.buildCommand) form.append("buildCommand", opts.buildCommand);
|
|
864
|
-
if (opts.port !== void 0) form.append("containerPort", String(opts.port));
|
|
865
|
-
if (opts.region) form.append("region", opts.region);
|
|
866
|
-
const result = await this.client.postForm(
|
|
867
|
-
`/api/projects/${opts.projectId}/apps/upload`,
|
|
868
|
-
form
|
|
869
|
-
);
|
|
870
|
-
return new DeployHandle(this.client, result.id, result.buildId);
|
|
871
|
-
}
|
|
872
|
-
/** Trigger a redeploy (rebuild from current source). */
|
|
873
|
-
async redeploy(id) {
|
|
874
|
-
await this.client.post(`/api/apps/${id}/redeploy`, {});
|
|
875
|
-
return new DeployHandle(this.client, id);
|
|
876
|
-
}
|
|
877
|
-
/** Restart a service without rebuilding. */
|
|
878
|
-
restart(id) {
|
|
879
|
-
return this.client.post(`/api/apps/${id}/restart`, {});
|
|
880
|
-
}
|
|
881
|
-
/** Scale a service. */
|
|
882
|
-
scale(id, opts) {
|
|
883
|
-
return this.client.post(`/api/apps/${id}/scale`, opts);
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Get recent log lines. For a live tail, use the WebSocket API instead.
|
|
887
|
-
*
|
|
888
|
-
* @param opts.limit - Max lines to return (default 200, max 1000).
|
|
889
|
-
*/
|
|
890
|
-
async logs(id, opts) {
|
|
891
|
-
const qs = new URLSearchParams();
|
|
892
|
-
if (opts?.limit) qs.set("limit", String(opts.limit));
|
|
893
|
-
if (opts?.since) qs.set("since", opts.since);
|
|
894
|
-
const result = await this.client.get(`/api/apps/${id}/logs?${qs}`);
|
|
895
|
-
return Array.isArray(result) ? result : result.logs ?? [];
|
|
896
|
-
}
|
|
897
|
-
/** Execute a command inside the running service container. */
|
|
898
|
-
exec(id, cmd, opts) {
|
|
899
|
-
return this.client.post(`/api/apps/${id}/exec`, { cmd, timeoutMs: opts?.timeoutMs });
|
|
900
|
-
}
|
|
901
|
-
/** Update service configuration. */
|
|
902
|
-
update(id, opts) {
|
|
903
|
-
return this.client.patch(`/api/apps/${id}`, opts);
|
|
904
|
-
}
|
|
905
|
-
/** Delete a service. */
|
|
906
|
-
delete(id) {
|
|
907
|
-
return this.client.delete(`/api/apps/${id}`);
|
|
1097
|
+
streamLogs(id, signal) {
|
|
1098
|
+
return this.client.events(`/api/projects/${id}/logs/stream`, { signal });
|
|
908
1099
|
}
|
|
909
1100
|
};
|
|
910
1101
|
|
|
@@ -919,7 +1110,11 @@ var AddonsAPI = class {
|
|
|
919
1110
|
}
|
|
920
1111
|
/** Get an addon by ID. */
|
|
921
1112
|
get(projectId, addonId) {
|
|
922
|
-
return this.
|
|
1113
|
+
return this.list({ projectId }).then((items) => {
|
|
1114
|
+
const addon = items.find((a) => a.id === addonId);
|
|
1115
|
+
if (!addon) throw new Error("Addon not found");
|
|
1116
|
+
return addon;
|
|
1117
|
+
});
|
|
923
1118
|
}
|
|
924
1119
|
/**
|
|
925
1120
|
* Create a new managed addon (database, cache, or object store).
|
|
@@ -933,12 +1128,15 @@ var AddonsAPI = class {
|
|
|
933
1128
|
*/
|
|
934
1129
|
create(opts) {
|
|
935
1130
|
return this.client.post(`/api/projects/${opts.projectId}/addons`, {
|
|
936
|
-
type: opts.type,
|
|
1131
|
+
type: opts.type === "mongodb" ? "mongo" : opts.type,
|
|
937
1132
|
name: opts.name,
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1133
|
+
region: opts.region,
|
|
1134
|
+
config: {
|
|
1135
|
+
version: opts.version,
|
|
1136
|
+
cpuLimit: opts.vcpu === void 0 ? void 0 : `${opts.vcpu * 1e3}m`,
|
|
1137
|
+
memoryLimit: opts.memoryMb === void 0 ? void 0 : `${opts.memoryMb}Mi`,
|
|
1138
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1139
|
+
}
|
|
942
1140
|
});
|
|
943
1141
|
}
|
|
944
1142
|
/** Delete an addon. */
|
|
@@ -946,13 +1144,29 @@ var AddonsAPI = class {
|
|
|
946
1144
|
return this.client.delete(`/api/projects/${projectId}/addons/${addonId}`);
|
|
947
1145
|
}
|
|
948
1146
|
/** Resize an addon (CPU / memory / storage). */
|
|
949
|
-
resize(projectId, addonId, opts) {
|
|
950
|
-
|
|
1147
|
+
async resize(projectId, addonId, opts) {
|
|
1148
|
+
await this.client.applyConfig(projectId, {
|
|
1149
|
+
addons: [{
|
|
1150
|
+
id: addonId,
|
|
1151
|
+
limits: { vcpu: opts.vcpu, memoryMb: opts.memoryMb },
|
|
1152
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1153
|
+
}]
|
|
1154
|
+
});
|
|
1155
|
+
return this.get(projectId, addonId);
|
|
951
1156
|
}
|
|
952
|
-
/** Restart an addon
|
|
1157
|
+
/** Restart an addon. */
|
|
953
1158
|
redeploy(projectId, addonId) {
|
|
954
1159
|
return this.client.post(`/api/projects/${projectId}/addons/${addonId}/redeploy`, {});
|
|
955
1160
|
}
|
|
1161
|
+
rename(projectId, addonId, name) {
|
|
1162
|
+
return this.client.patch(`/api/projects/${projectId}/addons/${addonId}`, { name });
|
|
1163
|
+
}
|
|
1164
|
+
secrets(projectId, addonId) {
|
|
1165
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/secrets`);
|
|
1166
|
+
}
|
|
1167
|
+
logs(projectId, addonId) {
|
|
1168
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/logs`);
|
|
1169
|
+
}
|
|
956
1170
|
};
|
|
957
1171
|
|
|
958
1172
|
// src/platform/secrets.ts
|
|
@@ -995,22 +1209,25 @@ var SecretsAPI = class {
|
|
|
995
1209
|
byService.get(s.serviceId)[s.key] = s.value;
|
|
996
1210
|
}
|
|
997
1211
|
}
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1212
|
+
const services = {};
|
|
1213
|
+
for (const [id, values] of byService) {
|
|
1214
|
+
const svc = await this.client.get(`/api/apps/${id}`);
|
|
1215
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1216
|
+
services[svc.name] = values;
|
|
1001
1217
|
}
|
|
1002
|
-
|
|
1003
|
-
tasks.push(this.client.post(`/api/apps/${serviceId}/secrets`, kv));
|
|
1004
|
-
}
|
|
1005
|
-
await Promise.all(tasks);
|
|
1218
|
+
await this.client.applyConfig(projectId, { secrets: { shared: projectScoped, services } });
|
|
1006
1219
|
}
|
|
1007
|
-
/** Delete a secret by key. */
|
|
1008
1220
|
async delete(projectId, opts) {
|
|
1221
|
+
const secrets = {};
|
|
1009
1222
|
if (opts.serviceId) {
|
|
1010
|
-
await this.client.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1223
|
+
const svc = await this.client.get(`/api/apps/${opts.serviceId}`);
|
|
1224
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1225
|
+
secrets.services = { [svc.name]: { [opts.key]: null } };
|
|
1226
|
+
} else secrets.shared = { [opts.key]: null };
|
|
1227
|
+
await this.client.applyConfig(projectId, { secrets });
|
|
1228
|
+
}
|
|
1229
|
+
refs(projectId, serviceId) {
|
|
1230
|
+
return this.client.get(serviceId ? `/api/apps/${serviceId}/variables:refs` : `/api/projects/${projectId}/variables:refs`);
|
|
1014
1231
|
}
|
|
1015
1232
|
};
|
|
1016
1233
|
|
|
@@ -1023,7 +1240,14 @@ var DomainsAPI = class {
|
|
|
1023
1240
|
* Show the current domain(s) for a service.
|
|
1024
1241
|
* The platform-assigned `.onlizard.com` subdomain is always present.
|
|
1025
1242
|
*/
|
|
1026
|
-
list(serviceId) {
|
|
1243
|
+
async list(serviceId) {
|
|
1244
|
+
const result = await this.info(serviceId);
|
|
1245
|
+
return [
|
|
1246
|
+
...result.domains.map((domain) => ({ domain, verified: true, live: result.dnsOk?.[domain], cnameTarget: result.cnameTarget })),
|
|
1247
|
+
...result.pending.map((item) => ({ ...item, domain: item.hostname, verified: false }))
|
|
1248
|
+
];
|
|
1249
|
+
}
|
|
1250
|
+
info(serviceId) {
|
|
1027
1251
|
return this.client.get(`/api/apps/${serviceId}/domains`);
|
|
1028
1252
|
}
|
|
1029
1253
|
/**
|
|
@@ -1036,15 +1260,23 @@ var DomainsAPI = class {
|
|
|
1036
1260
|
* console.log('Add CNAME:', info.cnameTarget)
|
|
1037
1261
|
* ```
|
|
1038
1262
|
*/
|
|
1039
|
-
add(serviceId, domain) {
|
|
1040
|
-
|
|
1263
|
+
async add(serviceId, domain, opts = {}) {
|
|
1264
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains`, { hostname: domain, ...opts });
|
|
1265
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
1041
1266
|
}
|
|
1042
1267
|
/**
|
|
1043
1268
|
* Verify that DNS records have propagated and activate the domain.
|
|
1044
1269
|
* Call after adding the CNAME or TXT record returned by `add()`.
|
|
1045
1270
|
*/
|
|
1046
|
-
verify(serviceId, domain) {
|
|
1047
|
-
|
|
1271
|
+
async verify(serviceId, domain) {
|
|
1272
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains/verify`, { hostname: domain });
|
|
1273
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
1274
|
+
}
|
|
1275
|
+
generate(serviceId) {
|
|
1276
|
+
return this.client.post(`/api/apps/${serviceId}/domains`, { generate: true });
|
|
1277
|
+
}
|
|
1278
|
+
delete(serviceId, hostname) {
|
|
1279
|
+
return this.client.delete(`/api/apps/${serviceId}/domains/${segment(hostname)}`);
|
|
1048
1280
|
}
|
|
1049
1281
|
};
|
|
1050
1282
|
|
|
@@ -1053,40 +1285,42 @@ var MetricsAPI = class {
|
|
|
1053
1285
|
constructor(client) {
|
|
1054
1286
|
this.client = client;
|
|
1055
1287
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
return this.client.get(`/api/apps/${id}/metrics/cpu?range=${range}`);
|
|
1288
|
+
service(id, range = "1h") {
|
|
1289
|
+
return this.client.get(`/api/apps/${id}/metrics?range=${range}`);
|
|
1059
1290
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
return this.client.get(`/api/apps/${id}/metrics/memory?range=${range}`);
|
|
1291
|
+
addon(projectId, addonId, range = "1h") {
|
|
1292
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/metrics?range=${range}`);
|
|
1063
1293
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return this.client.get(`/api/apps/${id}/metrics/network?range=${range}`);
|
|
1294
|
+
project(id, opts = {}) {
|
|
1295
|
+
return this.client.get(query(`/api/projects/${id}/metrics`, opts));
|
|
1067
1296
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1297
|
+
points(raw, name) {
|
|
1298
|
+
const series = raw.series.find((s) => s.metric === name);
|
|
1299
|
+
return (series?.values ?? []).flatMap((value, i) => series?.available?.[i] === false ? [] : [{ t: raw.timestamps[i], value }]);
|
|
1071
1300
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1301
|
+
async cpu(id, range = "1h") {
|
|
1302
|
+
return this.points(await this.service(id, range), "cpu");
|
|
1303
|
+
}
|
|
1304
|
+
async memory(id, range = "1h") {
|
|
1305
|
+
return this.points(await this.service(id, range), "memory");
|
|
1306
|
+
}
|
|
1307
|
+
async network(id, range = "1h") {
|
|
1308
|
+
const raw = await this.service(id, range);
|
|
1309
|
+
return { rx: this.points(raw, "network_rx"), tx: this.points(raw, "network_tx") };
|
|
1310
|
+
}
|
|
1311
|
+
async disk(id, range = "1h") {
|
|
1312
|
+
const raw = await this.service(id, range);
|
|
1313
|
+
return { read: this.points(raw, "disk_read"), write: this.points(raw, "disk_write") };
|
|
1078
1314
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Convenience: fetch all metric types for a service in one call.
|
|
1081
|
-
*/
|
|
1082
1315
|
async all(id, range = "1h") {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1316
|
+
const raw = await this.service(id, range);
|
|
1317
|
+
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") };
|
|
1318
|
+
}
|
|
1319
|
+
/** Return the backend's project cost row in USD, without guessing missing totals. */
|
|
1320
|
+
async cost(projectId, range = "30d") {
|
|
1321
|
+
const project = await this.client.get(`/api/projects/${projectId}`);
|
|
1322
|
+
const summary = await this.client.get(query("/api/billing/summary", { workspaceId: project.workspaceId, range }));
|
|
1323
|
+
return summary.projects.find((p) => p.projectId === projectId) ?? null;
|
|
1090
1324
|
}
|
|
1091
1325
|
};
|
|
1092
1326
|
|
|
@@ -1171,6 +1405,71 @@ var RegionsAPI = class {
|
|
|
1171
1405
|
}
|
|
1172
1406
|
};
|
|
1173
1407
|
|
|
1408
|
+
// src/cli.ts
|
|
1409
|
+
var import_node_child_process = require("child_process");
|
|
1410
|
+
var LizardCLI = class {
|
|
1411
|
+
constructor(opts = {}) {
|
|
1412
|
+
this.opts = opts;
|
|
1413
|
+
}
|
|
1414
|
+
run(args, opts = {}) {
|
|
1415
|
+
return new Promise((resolve, reject) => {
|
|
1416
|
+
const env = { ...process.env };
|
|
1417
|
+
if (this.opts.apiKey) {
|
|
1418
|
+
env.LIZARD_API_KEY = this.opts.apiKey;
|
|
1419
|
+
env.LIZARD_TOKEN = this.opts.apiKey;
|
|
1420
|
+
}
|
|
1421
|
+
if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
|
|
1422
|
+
const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
|
|
1423
|
+
cwd: this.opts.cwd,
|
|
1424
|
+
env,
|
|
1425
|
+
shell: false,
|
|
1426
|
+
stdio: "pipe",
|
|
1427
|
+
signal: opts.signal,
|
|
1428
|
+
timeout: this.opts.timeoutMs ?? 6e5
|
|
1429
|
+
});
|
|
1430
|
+
child.stdout.setEncoding("utf8");
|
|
1431
|
+
child.stderr.setEncoding("utf8");
|
|
1432
|
+
let stdout = "", stderr = "", bytes = 0;
|
|
1433
|
+
const collect = (stream, data) => {
|
|
1434
|
+
bytes += Buffer.byteLength(data);
|
|
1435
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
1436
|
+
child.kill();
|
|
1437
|
+
reject(new Error("CLI output exceeds 16 MiB"));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (stream === "stdout") stdout += data;
|
|
1441
|
+
else stderr += data;
|
|
1442
|
+
};
|
|
1443
|
+
child.stdout.on("data", (data) => collect("stdout", data));
|
|
1444
|
+
child.stderr.on("data", (data) => collect("stderr", data));
|
|
1445
|
+
child.on("error", reject);
|
|
1446
|
+
child.on("close", (code, signal) => {
|
|
1447
|
+
if (signal) {
|
|
1448
|
+
reject(new Error(`CLI ended with signal ${signal}`));
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
let events = [];
|
|
1452
|
+
try {
|
|
1453
|
+
events = [JSON.parse(stdout)];
|
|
1454
|
+
} catch {
|
|
1455
|
+
events = stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
1456
|
+
try {
|
|
1457
|
+
return [JSON.parse(line)];
|
|
1458
|
+
} catch {
|
|
1459
|
+
return [];
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
resolve({ code: code ?? 1, stdout, stderr, events });
|
|
1464
|
+
});
|
|
1465
|
+
child.stdin.on("error", (error) => {
|
|
1466
|
+
if (error.code !== "EPIPE") reject(error);
|
|
1467
|
+
});
|
|
1468
|
+
child.stdin.end(opts.stdin);
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1174
1473
|
// src/platform/billing.ts
|
|
1175
1474
|
var BillingAPI = class {
|
|
1176
1475
|
constructor(client) {
|
|
@@ -1197,6 +1496,46 @@ var BillingAPI = class {
|
|
|
1197
1496
|
live() {
|
|
1198
1497
|
return this.client.get("/api/billing/live");
|
|
1199
1498
|
}
|
|
1499
|
+
paymentMethods() {
|
|
1500
|
+
return this.client.get("/api/billing/payment-methods");
|
|
1501
|
+
}
|
|
1502
|
+
setupPaymentMethod(returnUrl) {
|
|
1503
|
+
return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
|
|
1504
|
+
}
|
|
1505
|
+
removePaymentMethod(id) {
|
|
1506
|
+
return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
|
|
1507
|
+
}
|
|
1508
|
+
purchase(creditCents, opts = {}) {
|
|
1509
|
+
if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
|
|
1510
|
+
return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
|
|
1511
|
+
}
|
|
1512
|
+
autoTopup() {
|
|
1513
|
+
return this.client.get("/api/billing/auto-topup");
|
|
1514
|
+
}
|
|
1515
|
+
setAutoTopup(opts) {
|
|
1516
|
+
return this.client.put("/api/billing/auto-topup", opts);
|
|
1517
|
+
}
|
|
1518
|
+
runAutoTopup() {
|
|
1519
|
+
return this.client.post("/api/billing/auto-topup/run", {});
|
|
1520
|
+
}
|
|
1521
|
+
redeemPromo(code) {
|
|
1522
|
+
return this.client.post("/api/billing/promo/redeem", { code });
|
|
1523
|
+
}
|
|
1524
|
+
x402Quote(creditCents) {
|
|
1525
|
+
return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
|
|
1526
|
+
}
|
|
1527
|
+
paymentStatus(attemptId) {
|
|
1528
|
+
return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
|
|
1529
|
+
}
|
|
1530
|
+
/** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
|
|
1531
|
+
async payX402(creditCents, opts) {
|
|
1532
|
+
if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
|
|
1533
|
+
const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
|
|
1534
|
+
if (opts.requestId) args.push("--request-id", opts.requestId);
|
|
1535
|
+
const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
|
|
1536
|
+
if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
|
|
1537
|
+
return result.events[0];
|
|
1538
|
+
}
|
|
1200
1539
|
};
|
|
1201
1540
|
|
|
1202
1541
|
// src/volume/index.ts
|
|
@@ -1323,6 +1662,8 @@ var Lizard = class {
|
|
|
1323
1662
|
this.secrets = new SecretsAPI(platform);
|
|
1324
1663
|
this.domains = new DomainsAPI(platform);
|
|
1325
1664
|
this.metrics = new MetricsAPI(platform);
|
|
1665
|
+
this.storage = new StorageAPI(platform);
|
|
1666
|
+
this.github = new GitHubAPI(platform);
|
|
1326
1667
|
}
|
|
1327
1668
|
// ── Sandbox convenience methods (backward-compatible) ──────────────────
|
|
1328
1669
|
/** Resolve the client's project reference to a stable project ID (cached). */
|
|
@@ -1415,8 +1756,7 @@ var CodeSandbox = class extends Sandbox {
|
|
|
1415
1756
|
*
|
|
1416
1757
|
* @returns Execution result with stdout, stderr, results, and any error.
|
|
1417
1758
|
*
|
|
1418
|
-
*
|
|
1419
|
-
* default context exists — which cannot happen in normal usage.
|
|
1759
|
+
* Code errors appear in `execution.error`; request failures throw.
|
|
1420
1760
|
*
|
|
1421
1761
|
* @example
|
|
1422
1762
|
* ```ts
|
|
@@ -1567,10 +1907,13 @@ async function* readLines(body) {
|
|
|
1567
1907
|
AuthenticationError,
|
|
1568
1908
|
BillingAPI,
|
|
1569
1909
|
CodeSandbox,
|
|
1910
|
+
ConfigApplyError,
|
|
1570
1911
|
ConflictError,
|
|
1571
1912
|
DeployHandle,
|
|
1572
1913
|
DomainsAPI,
|
|
1914
|
+
GitHubAPI,
|
|
1573
1915
|
Lizard,
|
|
1916
|
+
LizardCLI,
|
|
1574
1917
|
LizardError,
|
|
1575
1918
|
MetricsAPI,
|
|
1576
1919
|
NotFoundError,
|
|
@@ -1580,6 +1923,7 @@ async function* readLines(body) {
|
|
|
1580
1923
|
Sandbox,
|
|
1581
1924
|
SecretsAPI,
|
|
1582
1925
|
ServicesAPI,
|
|
1926
|
+
StorageAPI,
|
|
1583
1927
|
TimeoutError,
|
|
1584
1928
|
Volume,
|
|
1585
1929
|
WorkspacesAPI,
|