@sandblocks/cli 0.2.0 → 0.3.0
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/dist/cli.js +100 -12
- package/dist/cli.js.map +5 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11707,9 +11707,11 @@ async function up(args, dependencies) {
|
|
|
11707
11707
|
try {
|
|
11708
11708
|
await deployUnlocked(args, dependencies);
|
|
11709
11709
|
} catch (error) {
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11710
|
+
if (process.env.SANDBLOCKS_RETAIN_FAILED !== "true") {
|
|
11711
|
+
await down(args, dependencies).catch(() => {
|
|
11712
|
+
return;
|
|
11713
|
+
});
|
|
11714
|
+
}
|
|
11713
11715
|
throw error;
|
|
11714
11716
|
}
|
|
11715
11717
|
});
|
|
@@ -11831,9 +11833,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11831
11833
|
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
11832
11834
|
const stack = loaded.manifest.name ?? context.state.repository;
|
|
11833
11835
|
const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
|
|
11834
|
-
const targetHost = process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost;
|
|
11836
|
+
const targetHost = await resolveAssignedTargetHost(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
|
|
11835
11837
|
if (!targetHost)
|
|
11836
|
-
throw new Error("
|
|
11838
|
+
throw new Error("assigned host targetHost label or preview targetHost is required");
|
|
11837
11839
|
const deploymentId = randomUUID();
|
|
11838
11840
|
const environmentSnapshotId = await createEnvironmentSnapshot(context);
|
|
11839
11841
|
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
@@ -11901,6 +11903,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11901
11903
|
try {
|
|
11902
11904
|
if (preview.steps.length) {
|
|
11903
11905
|
const checkRoutes = sandbox.previewUrls ?? [];
|
|
11906
|
+
await waitForPublishedRoutes(checkRoutes);
|
|
11904
11907
|
const serviceUrls2 = Object.fromEntries(checkRoutes.flatMap((route) => route.service ? [
|
|
11905
11908
|
[
|
|
11906
11909
|
`SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`,
|
|
@@ -11931,8 +11934,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11931
11934
|
});
|
|
11932
11935
|
const checked = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
|
|
11933
11936
|
checkEvidence = checked.result?.checks ?? [];
|
|
11934
|
-
if (checked.result?.passed !== true)
|
|
11935
|
-
throw new Error(
|
|
11937
|
+
if (checked.result?.passed !== true) {
|
|
11938
|
+
throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
|
|
11939
|
+
}
|
|
11936
11940
|
sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
|
|
11937
11941
|
}
|
|
11938
11942
|
} catch (error) {
|
|
@@ -12054,6 +12058,14 @@ async function down(args, dependencies) {
|
|
|
12054
12058
|
await rm(context.file, { force: true });
|
|
12055
12059
|
console.log(`sandbox ${context.state.sandboxId} destroyed`);
|
|
12056
12060
|
}
|
|
12061
|
+
async function resolveAssignedTargetHost(context, fallback) {
|
|
12062
|
+
if (fallback?.trim())
|
|
12063
|
+
return fallback.trim();
|
|
12064
|
+
const body = await request(context.apiUrl, context.apiKey, "/v1/hosts");
|
|
12065
|
+
const host = Array.isArray(body.hosts) ? body.hosts.find((candidate) => candidate?.id === context.state.hostId) : undefined;
|
|
12066
|
+
const targetHost = host?.labels?.targetHost;
|
|
12067
|
+
return typeof targetHost === "string" && targetHost.trim() ? targetHost.trim() : undefined;
|
|
12068
|
+
}
|
|
12057
12069
|
async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status2) {
|
|
12058
12070
|
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
|
|
12059
12071
|
method: "POST",
|
|
@@ -12122,7 +12134,7 @@ async function destroyDeploymentById(context, deploymentId) {
|
|
|
12122
12134
|
await wait(context.apiUrl, context.apiKey, body.operation.id);
|
|
12123
12135
|
}
|
|
12124
12136
|
async function destroyWorkspace(context, idempotencyKey) {
|
|
12125
|
-
const response = await
|
|
12137
|
+
const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
|
|
12126
12138
|
method: "DELETE",
|
|
12127
12139
|
headers: {
|
|
12128
12140
|
"content-type": "application/json",
|
|
@@ -12139,7 +12151,7 @@ async function destroyWorkspace(context, idempotencyKey) {
|
|
|
12139
12151
|
}
|
|
12140
12152
|
async function importSource(input) {
|
|
12141
12153
|
const source = await input.source(input.root);
|
|
12142
|
-
const response = await
|
|
12154
|
+
const response = await fetchWithRetry(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
|
|
12143
12155
|
method: "POST",
|
|
12144
12156
|
headers: {
|
|
12145
12157
|
"content-type": "application/x-tar",
|
|
@@ -12202,7 +12214,7 @@ async function request(apiUrl, apiKey, pathname, init = {}) {
|
|
|
12202
12214
|
headers.set("x-sandblocks-api-key", apiKey);
|
|
12203
12215
|
if (init.body)
|
|
12204
12216
|
headers.set("content-type", "application/json");
|
|
12205
|
-
const response = await
|
|
12217
|
+
const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
|
|
12206
12218
|
const body = await response.json().catch(() => ({}));
|
|
12207
12219
|
if (!response.ok)
|
|
12208
12220
|
throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
|
|
@@ -12213,12 +12225,61 @@ async function leaseRequest(context, pathname, init = {}) {
|
|
|
12213
12225
|
headers.set("authorization", `Bearer ${context.state.leaseToken}`);
|
|
12214
12226
|
if (init.body)
|
|
12215
12227
|
headers.set("content-type", "application/json");
|
|
12216
|
-
const response = await
|
|
12228
|
+
const response = await fetchWithRetry(`${context.apiUrl}${pathname}`, { ...init, headers });
|
|
12217
12229
|
const body = await response.json().catch(() => ({}));
|
|
12218
12230
|
if (!response.ok)
|
|
12219
12231
|
throw new Error(String(body.error ?? `Sandblocks lease request failed (${response.status})`));
|
|
12220
12232
|
return body;
|
|
12221
12233
|
}
|
|
12234
|
+
async function waitForPublishedRoutes(routes, timeoutMs = 90000) {
|
|
12235
|
+
const deadline = Date.now() + timeoutMs;
|
|
12236
|
+
let pending = routes.map((route) => ({
|
|
12237
|
+
label: route.url,
|
|
12238
|
+
url: route.service === "api" ? new URL("/health", route.url).toString() : route.url
|
|
12239
|
+
}));
|
|
12240
|
+
while (pending.length && Date.now() < deadline) {
|
|
12241
|
+
const checks = await Promise.all(pending.map(async (route) => {
|
|
12242
|
+
try {
|
|
12243
|
+
const response = await fetch(route.url, { redirect: "manual" });
|
|
12244
|
+
await response.body?.cancel().catch(() => {
|
|
12245
|
+
return;
|
|
12246
|
+
});
|
|
12247
|
+
return response.status >= 200 && response.status < 400 ? undefined : route;
|
|
12248
|
+
} catch {
|
|
12249
|
+
return route;
|
|
12250
|
+
}
|
|
12251
|
+
}));
|
|
12252
|
+
pending = checks.filter((route) => typeof route === "object" && route !== null);
|
|
12253
|
+
if (pending.length)
|
|
12254
|
+
await Bun.sleep(2000);
|
|
12255
|
+
}
|
|
12256
|
+
if (pending.length) {
|
|
12257
|
+
throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
|
|
12258
|
+
}
|
|
12259
|
+
}
|
|
12260
|
+
async function fetchWithRetry(url, init = {}) {
|
|
12261
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
12262
|
+
const headers = new Headers(init.headers);
|
|
12263
|
+
const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
|
|
12264
|
+
const attempts = safeToRetry ? 8 : 1;
|
|
12265
|
+
let lastError;
|
|
12266
|
+
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
12267
|
+
try {
|
|
12268
|
+
const response = await fetch(url, init);
|
|
12269
|
+
if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
|
|
12270
|
+
return response;
|
|
12271
|
+
await response.body?.cancel().catch(() => {
|
|
12272
|
+
return;
|
|
12273
|
+
});
|
|
12274
|
+
} catch (error) {
|
|
12275
|
+
lastError = error;
|
|
12276
|
+
if (attempt === attempts - 1)
|
|
12277
|
+
throw error;
|
|
12278
|
+
}
|
|
12279
|
+
await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
|
|
12280
|
+
}
|
|
12281
|
+
throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
|
|
12282
|
+
}
|
|
12222
12283
|
async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
|
|
12223
12284
|
const deadline = Date.now() + timeoutMs;
|
|
12224
12285
|
while (Date.now() < deadline) {
|
|
@@ -14266,6 +14327,7 @@ var HELP = `Sandblocks CLI
|
|
|
14266
14327
|
sandblocks register [directory] --project <id> [--repository-url <url>]
|
|
14267
14328
|
[--api-url <url>] [--api-key <key>] [--json]
|
|
14268
14329
|
sandblocks doctor [directory] [--api-url <url>]
|
|
14330
|
+
sandblocks whoami [directory] [--api-url <url>] [--api-key <key>] [--json]
|
|
14269
14331
|
sandblocks sandbox up [directory] [--environment <id>] [--project <id>]
|
|
14270
14332
|
sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
|
|
14271
14333
|
sandblocks sandbox deploy [directory] [--environment <id>]
|
|
@@ -14311,6 +14373,8 @@ async function run2(argv = process.argv.slice(2)) {
|
|
|
14311
14373
|
await register(args);
|
|
14312
14374
|
else if (command === "doctor")
|
|
14313
14375
|
await doctor(args);
|
|
14376
|
+
else if (command === "whoami")
|
|
14377
|
+
await whoami(args);
|
|
14314
14378
|
else if (command === "sandbox")
|
|
14315
14379
|
await sandbox(args);
|
|
14316
14380
|
else if (command === "sdk")
|
|
@@ -14415,6 +14479,30 @@ async function doctor(args) {
|
|
|
14415
14479
|
const gitDetected = await stat3(path4.join(root, ".git")).then(() => true).catch(() => false);
|
|
14416
14480
|
console.log(`git ${gitDetected ? "ok" : "not detected"}`);
|
|
14417
14481
|
}
|
|
14482
|
+
async function whoami(args) {
|
|
14483
|
+
const options = parse(args);
|
|
14484
|
+
const apiUrl = (option(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
14485
|
+
const apiKey = option(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
14486
|
+
if (!apiUrl)
|
|
14487
|
+
throw new Error("whoami requires --api-url or SANDBLOCKS_API_URL");
|
|
14488
|
+
if (!apiKey)
|
|
14489
|
+
throw new Error("whoami requires --api-key or SANDBLOCKS_API_KEY");
|
|
14490
|
+
const response = await fetch(`${apiUrl}/v1/session`, {
|
|
14491
|
+
headers: { "x-sandblocks-api-key": apiKey }
|
|
14492
|
+
});
|
|
14493
|
+
const body = await response.json().catch(() => ({}));
|
|
14494
|
+
if (!response.ok)
|
|
14495
|
+
throw new Error(String(body.error ?? `session lookup failed (${response.status})`));
|
|
14496
|
+
if (options.json) {
|
|
14497
|
+
console.log(JSON.stringify(body, null, 2));
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
14500
|
+
const session = body.session ?? {};
|
|
14501
|
+
console.log(`subject ${session.subject ?? "unknown"}`);
|
|
14502
|
+
console.log(`source ${session.source ?? "unknown"}`);
|
|
14503
|
+
console.log(`project ${session.projectId ?? "unscoped"}`);
|
|
14504
|
+
console.log(`scopes ${(session.scopes ?? []).join(",") || "none"}`);
|
|
14505
|
+
}
|
|
14418
14506
|
async function sandbox(args) {
|
|
14419
14507
|
const [subcommand, ...rest] = args;
|
|
14420
14508
|
if (isStatefulSandboxCommand(subcommand) && !(subcommand === "deploy" && rest.includes("--workspace"))) {
|
|
@@ -14785,4 +14873,4 @@ export {
|
|
|
14785
14873
|
parse
|
|
14786
14874
|
};
|
|
14787
14875
|
|
|
14788
|
-
//# debugId=
|
|
14876
|
+
//# debugId=DA4A8E84000D88D364756E2164756E21
|