@sandblocks/cli 0.2.0 → 0.4.0-b.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 +136 -22
- package/dist/cli.js.map +8 -8
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -11117,7 +11117,7 @@ var EnvironmentValueSchema = exports_external.object({
|
|
|
11117
11117
|
routing: exports_external.object({
|
|
11118
11118
|
baseDomain: exports_external.string().min(1).max(253).default("sandblocks.dev"),
|
|
11119
11119
|
domains: exports_external.record(id, exports_external.string().min(1).max(253)).default({}),
|
|
11120
|
-
stableDomains: exports_external.record(id, exports_external.string().min(1).max(253)).default({}),
|
|
11120
|
+
stableDomains: exports_external.record(id, exports_external.union([exports_external.string().min(1).max(253), exports_external.array(exports_external.string().min(1).max(253)).min(1)])).default({}),
|
|
11121
11121
|
sso: exports_external.boolean().optional()
|
|
11122
11122
|
}).strict().default({})
|
|
11123
11123
|
}).strict();
|
|
@@ -11153,17 +11153,19 @@ function validateReferences(manifest, ctx) {
|
|
|
11153
11153
|
validateReferencesTo(ctx, environment.checks, checkIds, ["environments", index, "checks"], "check");
|
|
11154
11154
|
validateReferencesTo(ctx, Object.keys(environment.routing.domains), new Set(environment.services), ["environments", index, "routing", "domains"], "environment service");
|
|
11155
11155
|
validateReferencesTo(ctx, Object.keys(environment.routing.stableDomains), new Set(environment.services), ["environments", index, "routing", "stableDomains"], "environment service");
|
|
11156
|
-
for (const [service,
|
|
11156
|
+
for (const [service, configured] of Object.entries({
|
|
11157
11157
|
...environment.routing.domains,
|
|
11158
11158
|
...environment.routing.stableDomains
|
|
11159
11159
|
})) {
|
|
11160
|
-
const
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11160
|
+
for (const template of Array.isArray(configured) ? configured : [configured]) {
|
|
11161
|
+
const expanded = template.replaceAll(/\{(?:sandbox|revision|service)\}/g, "preview");
|
|
11162
|
+
if (expanded.includes("{") || !/^[a-z0-9.-]+$/.test(expanded.toLowerCase())) {
|
|
11163
|
+
ctx.addIssue({
|
|
11164
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11165
|
+
path: ["environments", index, "routing", "domains", service],
|
|
11166
|
+
message: "domain template may use only {sandbox}, {revision}, and {service}"
|
|
11167
|
+
});
|
|
11168
|
+
}
|
|
11167
11169
|
}
|
|
11168
11170
|
}
|
|
11169
11171
|
}
|
|
@@ -11707,9 +11709,11 @@ async function up(args, dependencies) {
|
|
|
11707
11709
|
try {
|
|
11708
11710
|
await deployUnlocked(args, dependencies);
|
|
11709
11711
|
} catch (error) {
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11712
|
+
if (process.env.SANDBLOCKS_RETAIN_FAILED !== "true") {
|
|
11713
|
+
await down(args, dependencies).catch(() => {
|
|
11714
|
+
return;
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11713
11717
|
throw error;
|
|
11714
11718
|
}
|
|
11715
11719
|
});
|
|
@@ -11831,9 +11835,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11831
11835
|
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
11832
11836
|
const stack = loaded.manifest.name ?? context.state.repository;
|
|
11833
11837
|
const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
|
|
11834
|
-
const targetHost = process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost;
|
|
11838
|
+
const targetHost = await resolveAssignedTargetHost(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
|
|
11835
11839
|
if (!targetHost)
|
|
11836
|
-
throw new Error("
|
|
11840
|
+
throw new Error("assigned host targetHost label or preview targetHost is required");
|
|
11837
11841
|
const deploymentId = randomUUID();
|
|
11838
11842
|
const environmentSnapshotId = await createEnvironmentSnapshot(context);
|
|
11839
11843
|
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
@@ -11901,6 +11905,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11901
11905
|
try {
|
|
11902
11906
|
if (preview.steps.length) {
|
|
11903
11907
|
const checkRoutes = sandbox.previewUrls ?? [];
|
|
11908
|
+
await waitForPublishedRoutes(checkRoutes);
|
|
11904
11909
|
const serviceUrls2 = Object.fromEntries(checkRoutes.flatMap((route) => route.service ? [
|
|
11905
11910
|
[
|
|
11906
11911
|
`SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`,
|
|
@@ -11931,8 +11936,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
11931
11936
|
});
|
|
11932
11937
|
const checked = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
|
|
11933
11938
|
checkEvidence = checked.result?.checks ?? [];
|
|
11934
|
-
if (checked.result?.passed !== true)
|
|
11935
|
-
throw new Error(
|
|
11939
|
+
if (checked.result?.passed !== true) {
|
|
11940
|
+
throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
|
|
11941
|
+
}
|
|
11936
11942
|
sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
|
|
11937
11943
|
}
|
|
11938
11944
|
} catch (error) {
|
|
@@ -12054,6 +12060,14 @@ async function down(args, dependencies) {
|
|
|
12054
12060
|
await rm(context.file, { force: true });
|
|
12055
12061
|
console.log(`sandbox ${context.state.sandboxId} destroyed`);
|
|
12056
12062
|
}
|
|
12063
|
+
async function resolveAssignedTargetHost(context, fallback) {
|
|
12064
|
+
if (fallback?.trim())
|
|
12065
|
+
return fallback.trim();
|
|
12066
|
+
const body = await request(context.apiUrl, context.apiKey, "/v1/hosts");
|
|
12067
|
+
const host = Array.isArray(body.hosts) ? body.hosts.find((candidate) => candidate?.id === context.state.hostId) : undefined;
|
|
12068
|
+
const targetHost = host?.labels?.targetHost;
|
|
12069
|
+
return typeof targetHost === "string" && targetHost.trim() ? targetHost.trim() : undefined;
|
|
12070
|
+
}
|
|
12057
12071
|
async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status2) {
|
|
12058
12072
|
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
|
|
12059
12073
|
method: "POST",
|
|
@@ -12122,7 +12136,7 @@ async function destroyDeploymentById(context, deploymentId) {
|
|
|
12122
12136
|
await wait(context.apiUrl, context.apiKey, body.operation.id);
|
|
12123
12137
|
}
|
|
12124
12138
|
async function destroyWorkspace(context, idempotencyKey) {
|
|
12125
|
-
const response = await
|
|
12139
|
+
const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
|
|
12126
12140
|
method: "DELETE",
|
|
12127
12141
|
headers: {
|
|
12128
12142
|
"content-type": "application/json",
|
|
@@ -12139,7 +12153,7 @@ async function destroyWorkspace(context, idempotencyKey) {
|
|
|
12139
12153
|
}
|
|
12140
12154
|
async function importSource(input) {
|
|
12141
12155
|
const source = await input.source(input.root);
|
|
12142
|
-
const response = await
|
|
12156
|
+
const response = await fetchWithRetry(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
|
|
12143
12157
|
method: "POST",
|
|
12144
12158
|
headers: {
|
|
12145
12159
|
"content-type": "application/x-tar",
|
|
@@ -12202,7 +12216,7 @@ async function request(apiUrl, apiKey, pathname, init = {}) {
|
|
|
12202
12216
|
headers.set("x-sandblocks-api-key", apiKey);
|
|
12203
12217
|
if (init.body)
|
|
12204
12218
|
headers.set("content-type", "application/json");
|
|
12205
|
-
const response = await
|
|
12219
|
+
const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
|
|
12206
12220
|
const body = await response.json().catch(() => ({}));
|
|
12207
12221
|
if (!response.ok)
|
|
12208
12222
|
throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
|
|
@@ -12213,12 +12227,61 @@ async function leaseRequest(context, pathname, init = {}) {
|
|
|
12213
12227
|
headers.set("authorization", `Bearer ${context.state.leaseToken}`);
|
|
12214
12228
|
if (init.body)
|
|
12215
12229
|
headers.set("content-type", "application/json");
|
|
12216
|
-
const response = await
|
|
12230
|
+
const response = await fetchWithRetry(`${context.apiUrl}${pathname}`, { ...init, headers });
|
|
12217
12231
|
const body = await response.json().catch(() => ({}));
|
|
12218
12232
|
if (!response.ok)
|
|
12219
12233
|
throw new Error(String(body.error ?? `Sandblocks lease request failed (${response.status})`));
|
|
12220
12234
|
return body;
|
|
12221
12235
|
}
|
|
12236
|
+
async function waitForPublishedRoutes(routes, timeoutMs = 90000) {
|
|
12237
|
+
const deadline = Date.now() + timeoutMs;
|
|
12238
|
+
let pending = routes.map((route) => ({
|
|
12239
|
+
label: route.url,
|
|
12240
|
+
url: route.service === "api" ? new URL("/health", route.url).toString() : route.url
|
|
12241
|
+
}));
|
|
12242
|
+
while (pending.length && Date.now() < deadline) {
|
|
12243
|
+
const checks = await Promise.all(pending.map(async (route) => {
|
|
12244
|
+
try {
|
|
12245
|
+
const response = await fetch(route.url, { redirect: "manual" });
|
|
12246
|
+
await response.body?.cancel().catch(() => {
|
|
12247
|
+
return;
|
|
12248
|
+
});
|
|
12249
|
+
return response.status >= 200 && response.status < 400 ? undefined : route;
|
|
12250
|
+
} catch {
|
|
12251
|
+
return route;
|
|
12252
|
+
}
|
|
12253
|
+
}));
|
|
12254
|
+
pending = checks.filter((route) => typeof route === "object" && route !== null);
|
|
12255
|
+
if (pending.length)
|
|
12256
|
+
await Bun.sleep(2000);
|
|
12257
|
+
}
|
|
12258
|
+
if (pending.length) {
|
|
12259
|
+
throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
|
|
12260
|
+
}
|
|
12261
|
+
}
|
|
12262
|
+
async function fetchWithRetry(url, init = {}) {
|
|
12263
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
12264
|
+
const headers = new Headers(init.headers);
|
|
12265
|
+
const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
|
|
12266
|
+
const attempts = safeToRetry ? 8 : 1;
|
|
12267
|
+
let lastError;
|
|
12268
|
+
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
12269
|
+
try {
|
|
12270
|
+
const response = await fetch(url, init);
|
|
12271
|
+
if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
|
|
12272
|
+
return response;
|
|
12273
|
+
await response.body?.cancel().catch(() => {
|
|
12274
|
+
return;
|
|
12275
|
+
});
|
|
12276
|
+
} catch (error) {
|
|
12277
|
+
lastError = error;
|
|
12278
|
+
if (attempt === attempts - 1)
|
|
12279
|
+
throw error;
|
|
12280
|
+
}
|
|
12281
|
+
await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
|
|
12282
|
+
}
|
|
12283
|
+
throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
|
|
12284
|
+
}
|
|
12222
12285
|
async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
|
|
12223
12286
|
const deadline = Date.now() + timeoutMs;
|
|
12224
12287
|
while (Date.now() < deadline) {
|
|
@@ -12277,7 +12340,8 @@ function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
|
|
|
12277
12340
|
return [];
|
|
12278
12341
|
const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
12279
12342
|
const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
|
|
12280
|
-
const
|
|
12343
|
+
const configuredStableHosts = preview.stableDomains[service.id];
|
|
12344
|
+
const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.deploymentType}-${stack}-${service.id}.${preview.baseDomain}`;
|
|
12281
12345
|
return [
|
|
12282
12346
|
[`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
|
|
12283
12347
|
[`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
|
|
@@ -14266,11 +14330,13 @@ var HELP = `Sandblocks CLI
|
|
|
14266
14330
|
sandblocks register [directory] --project <id> [--repository-url <url>]
|
|
14267
14331
|
[--api-url <url>] [--api-key <key>] [--json]
|
|
14268
14332
|
sandblocks doctor [directory] [--api-url <url>]
|
|
14333
|
+
sandblocks whoami [directory] [--api-url <url>] [--api-key <key>] [--json]
|
|
14269
14334
|
sandblocks sandbox up [directory] [--environment <id>] [--project <id>]
|
|
14270
14335
|
sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
|
|
14271
14336
|
sandblocks sandbox deploy [directory] [--environment <id>]
|
|
14272
14337
|
sandblocks sandbox redeploy [directory] [--environment <id>]
|
|
14273
14338
|
sandblocks sandbox status [directory] [--environment <id>] [--json]
|
|
14339
|
+
sandblocks sandbox promote --project <id> --sandbox <id> [--json]
|
|
14274
14340
|
sandblocks sandbox renew [directory] [--environment <id>] [--ttl-seconds <n>]
|
|
14275
14341
|
sandblocks sandbox exec [directory] [--environment <id>] -- <typed-command>
|
|
14276
14342
|
sandblocks sandbox git [directory] [--environment <id>] <status|diff|stage|checkout>
|
|
@@ -14311,6 +14377,8 @@ async function run2(argv = process.argv.slice(2)) {
|
|
|
14311
14377
|
await register(args);
|
|
14312
14378
|
else if (command === "doctor")
|
|
14313
14379
|
await doctor(args);
|
|
14380
|
+
else if (command === "whoami")
|
|
14381
|
+
await whoami(args);
|
|
14314
14382
|
else if (command === "sandbox")
|
|
14315
14383
|
await sandbox(args);
|
|
14316
14384
|
else if (command === "sdk")
|
|
@@ -14415,6 +14483,30 @@ async function doctor(args) {
|
|
|
14415
14483
|
const gitDetected = await stat3(path4.join(root, ".git")).then(() => true).catch(() => false);
|
|
14416
14484
|
console.log(`git ${gitDetected ? "ok" : "not detected"}`);
|
|
14417
14485
|
}
|
|
14486
|
+
async function whoami(args) {
|
|
14487
|
+
const options = parse(args);
|
|
14488
|
+
const apiUrl = (option(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
14489
|
+
const apiKey = option(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
14490
|
+
if (!apiUrl)
|
|
14491
|
+
throw new Error("whoami requires --api-url or SANDBLOCKS_API_URL");
|
|
14492
|
+
if (!apiKey)
|
|
14493
|
+
throw new Error("whoami requires --api-key or SANDBLOCKS_API_KEY");
|
|
14494
|
+
const response = await fetch(`${apiUrl}/v1/session`, {
|
|
14495
|
+
headers: { "x-sandblocks-api-key": apiKey }
|
|
14496
|
+
});
|
|
14497
|
+
const body = await response.json().catch(() => ({}));
|
|
14498
|
+
if (!response.ok)
|
|
14499
|
+
throw new Error(String(body.error ?? `session lookup failed (${response.status})`));
|
|
14500
|
+
if (options.json) {
|
|
14501
|
+
console.log(JSON.stringify(body, null, 2));
|
|
14502
|
+
return;
|
|
14503
|
+
}
|
|
14504
|
+
const session = body.session ?? {};
|
|
14505
|
+
console.log(`subject ${session.subject ?? "unknown"}`);
|
|
14506
|
+
console.log(`source ${session.source ?? "unknown"}`);
|
|
14507
|
+
console.log(`project ${session.projectId ?? "unscoped"}`);
|
|
14508
|
+
console.log(`scopes ${(session.scopes ?? []).join(",") || "none"}`);
|
|
14509
|
+
}
|
|
14418
14510
|
async function sandbox(args) {
|
|
14419
14511
|
const [subcommand, ...rest] = args;
|
|
14420
14512
|
if (isStatefulSandboxCommand(subcommand) && !(subcommand === "deploy" && rest.includes("--workspace"))) {
|
|
@@ -14430,6 +14522,8 @@ async function sandbox(args) {
|
|
|
14430
14522
|
}
|
|
14431
14523
|
if (subcommand === "deploy")
|
|
14432
14524
|
return sandboxDeploy(rest);
|
|
14525
|
+
if (subcommand === "promote")
|
|
14526
|
+
return sandboxPromote(rest);
|
|
14433
14527
|
if (subcommand !== "import")
|
|
14434
14528
|
throw new Error("unsupported sandbox command");
|
|
14435
14529
|
const options = parse(rest);
|
|
@@ -14474,6 +14568,26 @@ async function sandbox(args) {
|
|
|
14474
14568
|
console.log(`container ${operation.result.container}`);
|
|
14475
14569
|
}
|
|
14476
14570
|
}
|
|
14571
|
+
async function sandboxPromote(args) {
|
|
14572
|
+
const options = parse(args);
|
|
14573
|
+
const projectId = option(options, "project");
|
|
14574
|
+
const sandboxId = option(options, "sandbox");
|
|
14575
|
+
const apiUrl = (option(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
14576
|
+
const apiKey = option(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
14577
|
+
if (!projectId)
|
|
14578
|
+
throw new Error("sandbox promote requires --project");
|
|
14579
|
+
if (!sandboxId)
|
|
14580
|
+
throw new Error("sandbox promote requires --sandbox");
|
|
14581
|
+
if (!apiUrl)
|
|
14582
|
+
throw new Error("sandbox promote requires --api-url or SANDBLOCKS_API_URL");
|
|
14583
|
+
if (!apiKey)
|
|
14584
|
+
throw new Error("sandbox promote requires --api-key or SANDBLOCKS_API_KEY");
|
|
14585
|
+
const result = await sandblocksRequest(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/promote`, { method: "POST", body: JSON.stringify({ sandboxId }) });
|
|
14586
|
+
if (options.json)
|
|
14587
|
+
console.log(JSON.stringify(result, null, 2));
|
|
14588
|
+
else
|
|
14589
|
+
console.log(`promoted ${sandboxId} to production`);
|
|
14590
|
+
}
|
|
14477
14591
|
var sourceFileCount = 0;
|
|
14478
14592
|
async function createSourceTar(root) {
|
|
14479
14593
|
const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
@@ -14785,4 +14899,4 @@ export {
|
|
|
14785
14899
|
parse
|
|
14786
14900
|
};
|
|
14787
14901
|
|
|
14788
|
-
//# debugId=
|
|
14902
|
+
//# debugId=D1104053F8254F7864756E2164756E21
|