@lizard-build/sdk 0.1.38 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -16
- package/dist/index.d.mts +510 -254
- package/dist/index.d.ts +510 -254
- package/dist/index.js +600 -252
- package/dist/index.mjs +596 -252
- package/package.json +7 -4
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(
|
|
@@ -302,7 +653,7 @@ var Fs = class {
|
|
|
302
653
|
* ```
|
|
303
654
|
*/
|
|
304
655
|
async write(path, data, opts) {
|
|
305
|
-
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
656
|
+
const content = typeof data === "string" ? data : new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
306
657
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
307
658
|
method: "POST",
|
|
308
659
|
headers: this.config.headers,
|
|
@@ -336,6 +687,14 @@ var Fs = class {
|
|
|
336
687
|
* const entries = await sandbox.fs.list('/app')
|
|
337
688
|
* ```
|
|
338
689
|
*/
|
|
690
|
+
async readBytes(path, opts) {
|
|
691
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`);
|
|
692
|
+
url.searchParams.set("path", path);
|
|
693
|
+
if (opts?.user) url.searchParams.set("user", opts.user);
|
|
694
|
+
const res = await fetch(url, { headers: this.config.headers });
|
|
695
|
+
if (!res.ok) await handleApiError(res);
|
|
696
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
697
|
+
}
|
|
339
698
|
async list(path, opts) {
|
|
340
699
|
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/list`);
|
|
341
700
|
url.searchParams.set("path", path);
|
|
@@ -527,7 +886,8 @@ var SandboxClient = class {
|
|
|
527
886
|
}
|
|
528
887
|
static async listSandboxes(opts) {
|
|
529
888
|
const config = new ConnectionConfig(opts);
|
|
530
|
-
const
|
|
889
|
+
const path = opts?.projectId ? `/api/projects/${encodeURIComponent(opts.projectId)}/sandboxes` : "/api/sandboxes";
|
|
890
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
531
891
|
headers: config.headers
|
|
532
892
|
});
|
|
533
893
|
if (!res.ok) await handleApiError(res);
|
|
@@ -670,86 +1030,34 @@ var Sandbox = class extends SandboxClient {
|
|
|
670
1030
|
const { hostname } = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
|
|
671
1031
|
return hostname;
|
|
672
1032
|
}
|
|
673
|
-
|
|
674
|
-
return
|
|
1033
|
+
fork(opts = {}) {
|
|
1034
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/fork`, { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 });
|
|
675
1035
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
Sandbox.defaultTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
679
|
-
|
|
680
|
-
// src/platform/client.ts
|
|
681
|
-
var PlatformClient = class {
|
|
682
|
-
constructor(opts) {
|
|
683
|
-
this.config = new ConnectionConfig(opts);
|
|
1036
|
+
snapshot(name) {
|
|
1037
|
+
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name });
|
|
684
1038
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
headers: this.config.headers
|
|
688
|
-
});
|
|
689
|
-
if (!res.ok) await handleApiError(res);
|
|
690
|
-
return res.json();
|
|
1039
|
+
unexpose(port) {
|
|
1040
|
+
return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
|
|
691
1041
|
}
|
|
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();
|
|
1042
|
+
logs(opts = {}) {
|
|
1043
|
+
return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
|
|
700
1044
|
}
|
|
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();
|
|
1045
|
+
static snapshots(projectId, opts) {
|
|
1046
|
+
return new PlatformClient(opts ?? {}).get(`/api/projects/${projectId}/snapshots`);
|
|
709
1047
|
}
|
|
710
|
-
async
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
headers: this.config.headers,
|
|
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();
|
|
1048
|
+
static async restore(snapshotId, opts) {
|
|
1049
|
+
const result = await new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/fork`, { timeoutMs: opts?.timeoutMs ?? 0 });
|
|
1050
|
+
return new this({ ...opts, sandboxId: result.sandboxId ?? result.id });
|
|
719
1051
|
}
|
|
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();
|
|
1052
|
+
static deleteSnapshot(snapshotId, opts) {
|
|
1053
|
+
return new PlatformClient(opts ?? {}).delete(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
729
1054
|
}
|
|
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
|
-
}
|
|
1055
|
+
resolveOpts(opts) {
|
|
1056
|
+
return { ...this.connectionConfig, ...opts };
|
|
751
1057
|
}
|
|
752
1058
|
};
|
|
1059
|
+
Sandbox.defaultTemplate = "base";
|
|
1060
|
+
Sandbox.defaultTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
753
1061
|
|
|
754
1062
|
// src/platform/projects.ts
|
|
755
1063
|
var ProjectsAPI = class {
|
|
@@ -777,134 +1085,20 @@ var ProjectsAPI = class {
|
|
|
777
1085
|
delete(id) {
|
|
778
1086
|
return this.client.delete(`/api/projects/${id}`);
|
|
779
1087
|
}
|
|
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;
|
|
1088
|
+
apply(id, config) {
|
|
1089
|
+
return this.client.applyConfig(id, config);
|
|
791
1090
|
}
|
|
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`);
|
|
1091
|
+
services(id) {
|
|
1092
|
+
return this.client.get(`/api/projects/${id}/services`);
|
|
820
1093
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
constructor(client) {
|
|
824
|
-
this.client = client;
|
|
1094
|
+
volumeLimits(id) {
|
|
1095
|
+
return this.client.get(`/api/projects/${id}/volume-limits`);
|
|
825
1096
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return this.client.get(`/api/projects/${opts.projectId}/apps`);
|
|
1097
|
+
logs(id, opts = {}) {
|
|
1098
|
+
return this.client.get(query(`/api/projects/${id}/logs`, opts));
|
|
829
1099
|
}
|
|
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}`);
|
|
1100
|
+
streamLogs(id, signal) {
|
|
1101
|
+
return this.client.events(`/api/projects/${id}/logs/stream`, { signal });
|
|
908
1102
|
}
|
|
909
1103
|
};
|
|
910
1104
|
|
|
@@ -919,7 +1113,11 @@ var AddonsAPI = class {
|
|
|
919
1113
|
}
|
|
920
1114
|
/** Get an addon by ID. */
|
|
921
1115
|
get(projectId, addonId) {
|
|
922
|
-
return this.
|
|
1116
|
+
return this.list({ projectId }).then((items) => {
|
|
1117
|
+
const addon = items.find((a) => a.id === addonId);
|
|
1118
|
+
if (!addon) throw new Error("Addon not found");
|
|
1119
|
+
return addon;
|
|
1120
|
+
});
|
|
923
1121
|
}
|
|
924
1122
|
/**
|
|
925
1123
|
* Create a new managed addon (database, cache, or object store).
|
|
@@ -933,12 +1131,15 @@ var AddonsAPI = class {
|
|
|
933
1131
|
*/
|
|
934
1132
|
create(opts) {
|
|
935
1133
|
return this.client.post(`/api/projects/${opts.projectId}/addons`, {
|
|
936
|
-
type: opts.type,
|
|
1134
|
+
type: opts.type === "mongodb" ? "mongo" : opts.type,
|
|
937
1135
|
name: opts.name,
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1136
|
+
region: opts.region,
|
|
1137
|
+
config: {
|
|
1138
|
+
version: opts.version,
|
|
1139
|
+
cpuLimit: opts.vcpu === void 0 ? void 0 : `${opts.vcpu * 1e3}m`,
|
|
1140
|
+
memoryLimit: opts.memoryMb === void 0 ? void 0 : `${opts.memoryMb}Mi`,
|
|
1141
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1142
|
+
}
|
|
942
1143
|
});
|
|
943
1144
|
}
|
|
944
1145
|
/** Delete an addon. */
|
|
@@ -946,13 +1147,29 @@ var AddonsAPI = class {
|
|
|
946
1147
|
return this.client.delete(`/api/projects/${projectId}/addons/${addonId}`);
|
|
947
1148
|
}
|
|
948
1149
|
/** Resize an addon (CPU / memory / storage). */
|
|
949
|
-
resize(projectId, addonId, opts) {
|
|
950
|
-
|
|
1150
|
+
async resize(projectId, addonId, opts) {
|
|
1151
|
+
await this.client.applyConfig(projectId, {
|
|
1152
|
+
addons: [{
|
|
1153
|
+
id: addonId,
|
|
1154
|
+
limits: { vcpu: opts.vcpu, memoryMb: opts.memoryMb },
|
|
1155
|
+
storageSize: opts.storageGb === void 0 ? void 0 : `${opts.storageGb}Gi`
|
|
1156
|
+
}]
|
|
1157
|
+
});
|
|
1158
|
+
return this.get(projectId, addonId);
|
|
951
1159
|
}
|
|
952
1160
|
/** Restart an addon VM. */
|
|
953
1161
|
redeploy(projectId, addonId) {
|
|
954
1162
|
return this.client.post(`/api/projects/${projectId}/addons/${addonId}/redeploy`, {});
|
|
955
1163
|
}
|
|
1164
|
+
rename(projectId, addonId, name) {
|
|
1165
|
+
return this.client.patch(`/api/projects/${projectId}/addons/${addonId}`, { name });
|
|
1166
|
+
}
|
|
1167
|
+
secrets(projectId, addonId) {
|
|
1168
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/secrets`);
|
|
1169
|
+
}
|
|
1170
|
+
logs(projectId, addonId) {
|
|
1171
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/logs`);
|
|
1172
|
+
}
|
|
956
1173
|
};
|
|
957
1174
|
|
|
958
1175
|
// src/platform/secrets.ts
|
|
@@ -995,22 +1212,25 @@ var SecretsAPI = class {
|
|
|
995
1212
|
byService.get(s.serviceId)[s.key] = s.value;
|
|
996
1213
|
}
|
|
997
1214
|
}
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1215
|
+
const services = {};
|
|
1216
|
+
for (const [id, values] of byService) {
|
|
1217
|
+
const svc = await this.client.get(`/api/apps/${id}`);
|
|
1218
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1219
|
+
services[svc.name] = values;
|
|
1001
1220
|
}
|
|
1002
|
-
|
|
1003
|
-
tasks.push(this.client.post(`/api/apps/${serviceId}/secrets`, kv));
|
|
1004
|
-
}
|
|
1005
|
-
await Promise.all(tasks);
|
|
1221
|
+
await this.client.applyConfig(projectId, { secrets: { shared: projectScoped, services } });
|
|
1006
1222
|
}
|
|
1007
|
-
/** Delete a secret by key. */
|
|
1008
1223
|
async delete(projectId, opts) {
|
|
1224
|
+
const secrets = {};
|
|
1009
1225
|
if (opts.serviceId) {
|
|
1010
|
-
await this.client.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1226
|
+
const svc = await this.client.get(`/api/apps/${opts.serviceId}`);
|
|
1227
|
+
if (svc.projectId !== projectId) throw new Error("Service does not belong to this project");
|
|
1228
|
+
secrets.services = { [svc.name]: { [opts.key]: null } };
|
|
1229
|
+
} else secrets.shared = { [opts.key]: null };
|
|
1230
|
+
await this.client.applyConfig(projectId, { secrets });
|
|
1231
|
+
}
|
|
1232
|
+
refs(projectId, serviceId) {
|
|
1233
|
+
return this.client.get(serviceId ? `/api/apps/${serviceId}/variables:refs` : `/api/projects/${projectId}/variables:refs`);
|
|
1014
1234
|
}
|
|
1015
1235
|
};
|
|
1016
1236
|
|
|
@@ -1023,7 +1243,14 @@ var DomainsAPI = class {
|
|
|
1023
1243
|
* Show the current domain(s) for a service.
|
|
1024
1244
|
* The platform-assigned `.onlizard.com` subdomain is always present.
|
|
1025
1245
|
*/
|
|
1026
|
-
list(serviceId) {
|
|
1246
|
+
async list(serviceId) {
|
|
1247
|
+
const result = await this.info(serviceId);
|
|
1248
|
+
return [
|
|
1249
|
+
...result.domains.map((domain) => ({ domain, verified: true, live: result.dnsOk?.[domain], cnameTarget: result.cnameTarget })),
|
|
1250
|
+
...result.pending.map((item) => ({ ...item, domain: item.hostname, verified: false }))
|
|
1251
|
+
];
|
|
1252
|
+
}
|
|
1253
|
+
info(serviceId) {
|
|
1027
1254
|
return this.client.get(`/api/apps/${serviceId}/domains`);
|
|
1028
1255
|
}
|
|
1029
1256
|
/**
|
|
@@ -1036,15 +1263,23 @@ var DomainsAPI = class {
|
|
|
1036
1263
|
* console.log('Add CNAME:', info.cnameTarget)
|
|
1037
1264
|
* ```
|
|
1038
1265
|
*/
|
|
1039
|
-
add(serviceId, domain) {
|
|
1040
|
-
|
|
1266
|
+
async add(serviceId, domain, opts = {}) {
|
|
1267
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains`, { hostname: domain, ...opts });
|
|
1268
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
1041
1269
|
}
|
|
1042
1270
|
/**
|
|
1043
1271
|
* Verify that DNS records have propagated and activate the domain.
|
|
1044
1272
|
* Call after adding the CNAME or TXT record returned by `add()`.
|
|
1045
1273
|
*/
|
|
1046
|
-
verify(serviceId, domain) {
|
|
1047
|
-
|
|
1274
|
+
async verify(serviceId, domain) {
|
|
1275
|
+
const result = await this.client.post(`/api/apps/${serviceId}/domains/verify`, { hostname: domain });
|
|
1276
|
+
return { ...result, domain: result.hostname ?? domain };
|
|
1277
|
+
}
|
|
1278
|
+
generate(serviceId) {
|
|
1279
|
+
return this.client.post(`/api/apps/${serviceId}/domains`, { generate: true });
|
|
1280
|
+
}
|
|
1281
|
+
delete(serviceId, hostname) {
|
|
1282
|
+
return this.client.delete(`/api/apps/${serviceId}/domains/${segment(hostname)}`);
|
|
1048
1283
|
}
|
|
1049
1284
|
};
|
|
1050
1285
|
|
|
@@ -1053,40 +1288,42 @@ var MetricsAPI = class {
|
|
|
1053
1288
|
constructor(client) {
|
|
1054
1289
|
this.client = client;
|
|
1055
1290
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
return this.client.get(`/api/apps/${id}/metrics/cpu?range=${range}`);
|
|
1291
|
+
service(id, range = "1h") {
|
|
1292
|
+
return this.client.get(`/api/apps/${id}/metrics?range=${range}`);
|
|
1059
1293
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
return this.client.get(`/api/apps/${id}/metrics/memory?range=${range}`);
|
|
1294
|
+
addon(projectId, addonId, range = "1h") {
|
|
1295
|
+
return this.client.get(`/api/projects/${projectId}/addons/${addonId}/metrics?range=${range}`);
|
|
1063
1296
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return this.client.get(`/api/apps/${id}/metrics/network?range=${range}`);
|
|
1297
|
+
project(id, opts = {}) {
|
|
1298
|
+
return this.client.get(query(`/api/projects/${id}/metrics`, opts));
|
|
1067
1299
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1300
|
+
points(raw, name) {
|
|
1301
|
+
const series = raw.series.find((s) => s.metric === name);
|
|
1302
|
+
return (series?.values ?? []).flatMap((value, i) => series?.available?.[i] === false ? [] : [{ t: raw.timestamps[i], value }]);
|
|
1071
1303
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1304
|
+
async cpu(id, range = "1h") {
|
|
1305
|
+
return this.points(await this.service(id, range), "cpu");
|
|
1306
|
+
}
|
|
1307
|
+
async memory(id, range = "1h") {
|
|
1308
|
+
return this.points(await this.service(id, range), "memory");
|
|
1309
|
+
}
|
|
1310
|
+
async network(id, range = "1h") {
|
|
1311
|
+
const raw = await this.service(id, range);
|
|
1312
|
+
return { rx: this.points(raw, "network_rx"), tx: this.points(raw, "network_tx") };
|
|
1313
|
+
}
|
|
1314
|
+
async disk(id, range = "1h") {
|
|
1315
|
+
const raw = await this.service(id, range);
|
|
1316
|
+
return { read: this.points(raw, "disk_read"), write: this.points(raw, "disk_write") };
|
|
1078
1317
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Convenience: fetch all metric types for a service in one call.
|
|
1081
|
-
*/
|
|
1082
1318
|
async all(id, range = "1h") {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1319
|
+
const raw = await this.service(id, range);
|
|
1320
|
+
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") };
|
|
1321
|
+
}
|
|
1322
|
+
/** Return the backend's project cost row in USD, without guessing missing totals. */
|
|
1323
|
+
async cost(projectId, range = "30d") {
|
|
1324
|
+
const project = await this.client.get(`/api/projects/${projectId}`);
|
|
1325
|
+
const summary = await this.client.get(query("/api/billing/summary", { workspaceId: project.workspaceId, range }));
|
|
1326
|
+
return summary.projects.find((p) => p.projectId === projectId) ?? null;
|
|
1090
1327
|
}
|
|
1091
1328
|
};
|
|
1092
1329
|
|
|
@@ -1171,6 +1408,71 @@ var RegionsAPI = class {
|
|
|
1171
1408
|
}
|
|
1172
1409
|
};
|
|
1173
1410
|
|
|
1411
|
+
// src/cli.ts
|
|
1412
|
+
var import_node_child_process = require("child_process");
|
|
1413
|
+
var LizardCLI = class {
|
|
1414
|
+
constructor(opts = {}) {
|
|
1415
|
+
this.opts = opts;
|
|
1416
|
+
}
|
|
1417
|
+
run(args, opts = {}) {
|
|
1418
|
+
return new Promise((resolve, reject) => {
|
|
1419
|
+
const env = { ...process.env };
|
|
1420
|
+
if (this.opts.apiKey) {
|
|
1421
|
+
env.LIZARD_API_KEY = this.opts.apiKey;
|
|
1422
|
+
env.LIZARD_TOKEN = this.opts.apiKey;
|
|
1423
|
+
}
|
|
1424
|
+
if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
|
|
1425
|
+
const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
|
|
1426
|
+
cwd: this.opts.cwd,
|
|
1427
|
+
env,
|
|
1428
|
+
shell: false,
|
|
1429
|
+
stdio: "pipe",
|
|
1430
|
+
signal: opts.signal,
|
|
1431
|
+
timeout: this.opts.timeoutMs ?? 6e5
|
|
1432
|
+
});
|
|
1433
|
+
child.stdout.setEncoding("utf8");
|
|
1434
|
+
child.stderr.setEncoding("utf8");
|
|
1435
|
+
let stdout = "", stderr = "", bytes = 0;
|
|
1436
|
+
const collect = (stream, data) => {
|
|
1437
|
+
bytes += Buffer.byteLength(data);
|
|
1438
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
1439
|
+
child.kill();
|
|
1440
|
+
reject(new Error("CLI output exceeds 16 MiB"));
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (stream === "stdout") stdout += data;
|
|
1444
|
+
else stderr += data;
|
|
1445
|
+
};
|
|
1446
|
+
child.stdout.on("data", (data) => collect("stdout", data));
|
|
1447
|
+
child.stderr.on("data", (data) => collect("stderr", data));
|
|
1448
|
+
child.on("error", reject);
|
|
1449
|
+
child.on("close", (code, signal) => {
|
|
1450
|
+
if (signal) {
|
|
1451
|
+
reject(new Error(`CLI ended with signal ${signal}`));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
let events = [];
|
|
1455
|
+
try {
|
|
1456
|
+
events = [JSON.parse(stdout)];
|
|
1457
|
+
} catch {
|
|
1458
|
+
events = stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
1459
|
+
try {
|
|
1460
|
+
return [JSON.parse(line)];
|
|
1461
|
+
} catch {
|
|
1462
|
+
return [];
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
resolve({ code: code ?? 1, stdout, stderr, events });
|
|
1467
|
+
});
|
|
1468
|
+
child.stdin.on("error", (error) => {
|
|
1469
|
+
if (error.code !== "EPIPE") reject(error);
|
|
1470
|
+
});
|
|
1471
|
+
child.stdin.end(opts.stdin);
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1174
1476
|
// src/platform/billing.ts
|
|
1175
1477
|
var BillingAPI = class {
|
|
1176
1478
|
constructor(client) {
|
|
@@ -1197,6 +1499,46 @@ var BillingAPI = class {
|
|
|
1197
1499
|
live() {
|
|
1198
1500
|
return this.client.get("/api/billing/live");
|
|
1199
1501
|
}
|
|
1502
|
+
paymentMethods() {
|
|
1503
|
+
return this.client.get("/api/billing/payment-methods");
|
|
1504
|
+
}
|
|
1505
|
+
setupPaymentMethod(returnUrl) {
|
|
1506
|
+
return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
|
|
1507
|
+
}
|
|
1508
|
+
removePaymentMethod(id) {
|
|
1509
|
+
return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
|
|
1510
|
+
}
|
|
1511
|
+
purchase(creditCents, opts = {}) {
|
|
1512
|
+
if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
|
|
1513
|
+
return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
|
|
1514
|
+
}
|
|
1515
|
+
autoTopup() {
|
|
1516
|
+
return this.client.get("/api/billing/auto-topup");
|
|
1517
|
+
}
|
|
1518
|
+
setAutoTopup(opts) {
|
|
1519
|
+
return this.client.put("/api/billing/auto-topup", opts);
|
|
1520
|
+
}
|
|
1521
|
+
runAutoTopup() {
|
|
1522
|
+
return this.client.post("/api/billing/auto-topup/run", {});
|
|
1523
|
+
}
|
|
1524
|
+
redeemPromo(code) {
|
|
1525
|
+
return this.client.post("/api/billing/promo/redeem", { code });
|
|
1526
|
+
}
|
|
1527
|
+
x402Quote(creditCents) {
|
|
1528
|
+
return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
|
|
1529
|
+
}
|
|
1530
|
+
paymentStatus(attemptId) {
|
|
1531
|
+
return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
|
|
1532
|
+
}
|
|
1533
|
+
/** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
|
|
1534
|
+
async payX402(creditCents, opts) {
|
|
1535
|
+
if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
|
|
1536
|
+
const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
|
|
1537
|
+
if (opts.requestId) args.push("--request-id", opts.requestId);
|
|
1538
|
+
const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
|
|
1539
|
+
if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
|
|
1540
|
+
return result.events[0];
|
|
1541
|
+
}
|
|
1200
1542
|
};
|
|
1201
1543
|
|
|
1202
1544
|
// src/volume/index.ts
|
|
@@ -1323,6 +1665,8 @@ var Lizard = class {
|
|
|
1323
1665
|
this.secrets = new SecretsAPI(platform);
|
|
1324
1666
|
this.domains = new DomainsAPI(platform);
|
|
1325
1667
|
this.metrics = new MetricsAPI(platform);
|
|
1668
|
+
this.storage = new StorageAPI(platform);
|
|
1669
|
+
this.github = new GitHubAPI(platform);
|
|
1326
1670
|
}
|
|
1327
1671
|
// ── Sandbox convenience methods (backward-compatible) ──────────────────
|
|
1328
1672
|
/** Resolve the client's project reference to a stable project ID (cached). */
|
|
@@ -1567,10 +1911,13 @@ async function* readLines(body) {
|
|
|
1567
1911
|
AuthenticationError,
|
|
1568
1912
|
BillingAPI,
|
|
1569
1913
|
CodeSandbox,
|
|
1914
|
+
ConfigApplyError,
|
|
1570
1915
|
ConflictError,
|
|
1571
1916
|
DeployHandle,
|
|
1572
1917
|
DomainsAPI,
|
|
1918
|
+
GitHubAPI,
|
|
1573
1919
|
Lizard,
|
|
1920
|
+
LizardCLI,
|
|
1574
1921
|
LizardError,
|
|
1575
1922
|
MetricsAPI,
|
|
1576
1923
|
NotFoundError,
|
|
@@ -1580,6 +1927,7 @@ async function* readLines(body) {
|
|
|
1580
1927
|
Sandbox,
|
|
1581
1928
|
SecretsAPI,
|
|
1582
1929
|
ServicesAPI,
|
|
1930
|
+
StorageAPI,
|
|
1583
1931
|
TimeoutError,
|
|
1584
1932
|
Volume,
|
|
1585
1933
|
WorkspacesAPI,
|