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