@sandblocks/cli 0.1.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 CHANGED
@@ -11707,9 +11707,11 @@ async function up(args, dependencies) {
11707
11707
  try {
11708
11708
  await deployUnlocked(args, dependencies);
11709
11709
  } catch (error) {
11710
- await down(args, dependencies).catch(() => {
11711
- return;
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("preview targetHost or SANDBLOCKS_SANDBOX_TARGET_HOST is required");
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("one or more post-deploy checks failed");
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 fetch(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
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 fetch(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
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 fetch(`${apiUrl}${pathname}`, { ...init, headers });
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 fetch(`${context.apiUrl}${pathname}`, { ...init, headers });
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=AE8677F9C15C750D64756E2164756E21
14876
+ //# debugId=DA4A8E84000D88D364756E2164756E21