@sandblocks/cli 0.5.0-b.28 → 0.5.0-b.29

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
@@ -60,6 +60,24 @@ function compileProjectGraph(manifest) {
60
60
  timeoutSeconds: service.health.timeout,
61
61
  failureThreshold: service.health.failures
62
62
  }
63
+ } : {},
64
+ ...service.development ? {
65
+ development: {
66
+ command: normalizeCommand(service.development.command),
67
+ ...service.development.health ? {
68
+ health: {
69
+ path: service.development.health.path,
70
+ intervalSeconds: service.development.health.interval,
71
+ timeoutSeconds: service.development.health.timeout,
72
+ failureThreshold: service.development.health.failures
73
+ }
74
+ } : {},
75
+ restart: service.development.restart,
76
+ ...service.development.resources ? {
77
+ memory: service.development.resources.memory,
78
+ cpus: service.development.resources.cpus
79
+ } : {}
80
+ }
63
81
  } : {}
64
82
  };
65
83
  }),
@@ -69,11 +87,19 @@ function compileProjectGraph(manifest) {
69
87
  context: check.context,
70
88
  ...check.command ? { command: check.command } : {},
71
89
  timeoutSeconds: check.timeout,
90
+ ...check.application ? { application: check.application } : {},
91
+ profile: check.profile,
92
+ inputs: check.inputs,
93
+ required: check.required,
72
94
  memory: check.resources.memory,
73
95
  cpus: check.resources.cpus
74
96
  })),
75
97
  environments: manifest.environments.map((environment) => ({
76
98
  id: environment.id,
99
+ mode: environment.mode,
100
+ managedEnvironment: environment.managedEnvironment ?? environment.id,
101
+ branches: environment.branches,
102
+ checkProfile: environment.checkProfile,
77
103
  pipeline: environment.pipeline,
78
104
  services: environment.services,
79
105
  checks: environment.checks,
@@ -4076,9 +4102,41 @@ function validateReferences(manifest, ctx) {
4076
4102
  }
4077
4103
  validateAcyclicServices(manifest, ctx);
4078
4104
  for (const [index, environment] of manifest.environments.entries()) {
4105
+ if (environment.mode === "development" && environment.checkProfile === "none") {
4106
+ ctx.addIssue({
4107
+ code: exports_external.ZodIssueCode.custom,
4108
+ path: ["environments", index, "checkProfile"],
4109
+ message: "development environments must keep a smoke or full quality gate"
4110
+ });
4111
+ }
4112
+ if (environment.id === "prod" && environment.mode !== "immutable") {
4113
+ ctx.addIssue({
4114
+ code: exports_external.ZodIssueCode.custom,
4115
+ path: ["environments", index, "mode"],
4116
+ message: "the prod environment must be immutable"
4117
+ });
4118
+ }
4079
4119
  validateReferencesTo(ctx, environment.pipeline, taskIds, ["environments", index, "pipeline"], "task");
4080
4120
  validateReferencesTo(ctx, environment.services, serviceIds, ["environments", index, "services"], "service");
4081
4121
  validateReferencesTo(ctx, environment.checks, checkIds, ["environments", index, "checks"], "check");
4122
+ if (environment.mode === "development") {
4123
+ for (const serviceId of environment.services) {
4124
+ const serviceIndex = manifest.services.findIndex((service2) => service2.id === serviceId);
4125
+ const service = manifest.services[serviceIndex];
4126
+ if (service && !service.development) {
4127
+ ctx.addIssue({
4128
+ code: exports_external.ZodIssueCode.custom,
4129
+ path: ["services", serviceIndex, "development"],
4130
+ message: `service '${serviceId}' requires a development command`
4131
+ });
4132
+ }
4133
+ }
4134
+ }
4135
+ }
4136
+ for (const [index, check] of manifest.checks.entries()) {
4137
+ if (check.application && !serviceIds.has(check.application)) {
4138
+ addReferenceIssue(ctx, ["checks", index, "application"], "service", check.application);
4139
+ }
4082
4140
  }
4083
4141
  }
4084
4142
  function validateAcyclicServices(manifest, ctx) {
@@ -4145,7 +4203,7 @@ function isSafeRepositoryPath(value) {
4145
4203
  return false;
4146
4204
  return !value.split("/").some((segment) => segment === "..");
4147
4205
  }
4148
- var id, repositoryPath, localImage, imageReference, ProjectCommandSchema, duration, resources, WorkspaceValueSchema, WorkspaceSchema, TaskValueSchema, TaskSchema, TasksV2Schema, HealthValueSchema, HealthV2Schema, ServiceValueSchema, ServicesV2Schema, CheckValueSchema, ChecksV2Schema, EnvironmentValueSchema, EnvironmentsV2Schema, HooksV2Schema, ManifestV2Fields, SandblocksManifestV2DocumentSchema, CompleteSandblocksManifestV2Schema, SandblocksManifestV2Schema;
4206
+ var id, repositoryPath, localImage, imageReference, ProjectCommandSchema, duration, resources, WorkspaceValueSchema, WorkspaceSchema, TaskValueSchema, TaskSchema, TasksV2Schema, HealthValueSchema, HealthV2Schema, DevelopmentServiceSchema, ServiceValueSchema, ServicesV2Schema, CheckValueSchema, ChecksV2Schema, EnvironmentValueSchema, EnvironmentsV2Schema, HooksV2Schema, ManifestV2Fields, SandblocksManifestV2DocumentSchema, CompleteSandblocksManifestV2Schema, SandblocksManifestV2Schema;
4149
4207
  var init_schema = __esm(() => {
4150
4208
  init_zod();
4151
4209
  id = exports_external.string().min(1).max(63).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be a lowercase kebab-case identifier");
@@ -4207,12 +4265,19 @@ var init_schema = __esm(() => {
4207
4265
  exports_external.string().startsWith("/").transform((path) => ({ path, interval: 10, timeout: 5, failures: 3 })),
4208
4266
  HealthValueSchema
4209
4267
  ]);
4268
+ DevelopmentServiceSchema = exports_external.object({
4269
+ command: ProjectCommandSchema,
4270
+ health: HealthV2Schema.optional(),
4271
+ restart: exports_external.enum(["no", "on-failure", "unless-stopped"]).default("on-failure"),
4272
+ resources: resources.optional()
4273
+ }).strict();
4210
4274
  ServiceValueSchema = exports_external.object({
4211
4275
  root: repositoryPath.default("."),
4212
4276
  command: ProjectCommandSchema.optional(),
4213
4277
  static: repositoryPath.optional(),
4214
4278
  port: exports_external.number().int().positive().max(65535).optional(),
4215
4279
  health: HealthV2Schema.optional(),
4280
+ development: DevelopmentServiceSchema.optional(),
4216
4281
  dependsOn: exports_external.array(id).default([]),
4217
4282
  resources: resources.default({})
4218
4283
  }).strict().superRefine((service, ctx) => {
@@ -4237,6 +4302,13 @@ var init_schema = __esm(() => {
4237
4302
  message: "static services use Sandblocks's managed health check"
4238
4303
  });
4239
4304
  }
4305
+ if (service.static && service.development) {
4306
+ ctx.addIssue({
4307
+ code: exports_external.ZodIssueCode.custom,
4308
+ path: ["development"],
4309
+ message: "static services cannot declare a development process"
4310
+ });
4311
+ }
4240
4312
  });
4241
4313
  ServicesV2Schema = keyedCollection(ServiceValueSchema);
4242
4314
  CheckValueSchema = exports_external.object({
@@ -4244,13 +4316,21 @@ var init_schema = __esm(() => {
4244
4316
  context: repositoryPath.default("."),
4245
4317
  command: exports_external.array(exports_external.string().min(1).max(2048)).max(64).optional(),
4246
4318
  timeout: duration.default(300),
4319
+ application: id.optional(),
4320
+ profile: exports_external.enum(["smoke", "full"]).default("full"),
4321
+ inputs: exports_external.array(repositoryPath).default([]),
4322
+ required: exports_external.boolean().default(true),
4247
4323
  resources: resources.default({ memory: "1g", cpus: "2" })
4248
4324
  }).strict();
4249
4325
  ChecksV2Schema = keyedCollection(CheckValueSchema);
4250
4326
  EnvironmentValueSchema = exports_external.object({
4327
+ mode: exports_external.enum(["immutable", "development"]).default("immutable"),
4328
+ managedEnvironment: id.optional(),
4329
+ branches: exports_external.array(exports_external.string().min(1).max(255)).max(32).default([]),
4251
4330
  pipeline: exports_external.array(id).default([]),
4252
4331
  services: exports_external.array(id).min(1),
4253
4332
  checks: exports_external.array(id).default([]),
4333
+ checkProfile: exports_external.enum(["none", "smoke", "full"]).default("full"),
4254
4334
  targetHost: exports_external.string().min(1).max(255).optional(),
4255
4335
  routing: exports_external.object({ sso: exports_external.boolean().optional() }).strict().default({})
4256
4336
  }).strict();
@@ -4295,6 +4375,8 @@ function resolveServiceSteps(manifest, environmentId) {
4295
4375
  command: graph.workspace.setup,
4296
4376
  workingDirectory: graph.workspace.root,
4297
4377
  timeoutSeconds: 1800,
4378
+ inputs: ["package.json", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"],
4379
+ outputs: [],
4298
4380
  required: true
4299
4381
  });
4300
4382
  }
@@ -4307,6 +4389,8 @@ function resolveServiceSteps(manifest, environmentId) {
4307
4389
  command: task.command,
4308
4390
  workingDirectory: task.root,
4309
4391
  timeoutSeconds: task.timeoutSeconds,
4392
+ inputs: task.inputs,
4393
+ outputs: task.outputs,
4310
4394
  required: true
4311
4395
  });
4312
4396
  }
@@ -4319,6 +4403,10 @@ function resolveEnvironment(manifest, id2) {
4319
4403
  const checks = new Map(graph.checks.map((check) => [check.id, check]));
4320
4404
  return {
4321
4405
  id: environment.id,
4406
+ mode: environment.mode,
4407
+ managedEnvironment: environment.managedEnvironment,
4408
+ branches: environment.branches,
4409
+ checkProfile: environment.checkProfile,
4322
4410
  goal: `${environment.id} sandbox`,
4323
4411
  deploymentType: environment.id,
4324
4412
  baseDomain: environment.baseDomain,
@@ -4334,7 +4422,7 @@ function resolveEnvironment(manifest, id2) {
4334
4422
  const service = services.get(serviceId);
4335
4423
  if (!service)
4336
4424
  throw new Error(`environment '${id2}' references unknown service '${serviceId}'`);
4337
- return executionService(service);
4425
+ return executionService(service, environment.mode);
4338
4426
  }),
4339
4427
  steps: environment.checks.map((checkId) => {
4340
4428
  const check = checks.get(checkId);
@@ -4346,6 +4434,10 @@ function resolveEnvironment(manifest, id2) {
4346
4434
  context: check.context,
4347
4435
  ...check.command ? { command: check.command } : {},
4348
4436
  timeoutSeconds: check.timeoutSeconds,
4437
+ ...check.application ? { application: check.application } : {},
4438
+ profile: check.profile,
4439
+ inputs: check.inputs,
4440
+ required: check.required,
4349
4441
  memory: check.memory,
4350
4442
  cpus: check.cpus
4351
4443
  };
@@ -4358,7 +4450,7 @@ function selectEnvironment(graph, id2) {
4358
4450
  throw new Error(`sandblocks.yml does not define environment '${id2}'`);
4359
4451
  return environment;
4360
4452
  }
4361
- function executionService(service) {
4453
+ function executionService(service, mode) {
4362
4454
  if (service.kind === "static") {
4363
4455
  return {
4364
4456
  id: service.id,
@@ -4368,15 +4460,19 @@ function executionService(service) {
4368
4460
  cpus: service.cpus
4369
4461
  };
4370
4462
  }
4463
+ const development = mode === "development" ? service.development : undefined;
4464
+ if (mode === "development" && !development) {
4465
+ throw new Error(`service '${service.id}' has no development command`);
4466
+ }
4371
4467
  return {
4372
4468
  id: service.id,
4373
4469
  kind: "process",
4374
4470
  root: service.root,
4375
- command: service.command,
4471
+ command: development?.command ?? service.command,
4376
4472
  ...service.port ? { port: service.port } : {},
4377
- ...service.health ? { healthPath: service.health.path } : {},
4378
- memory: service.memory,
4379
- cpus: service.cpus
4473
+ ...development?.health ? { healthPath: development.health.path } : service.health ? { healthPath: service.health.path } : {},
4474
+ memory: development?.memory ?? service.memory,
4475
+ cpus: development?.cpus ?? service.cpus
4380
4476
  };
4381
4477
  }
4382
4478
  var init_resolve = () => {};
@@ -11719,7 +11815,7 @@ var init_src = __esm(() => {
11719
11815
 
11720
11816
  // packages/libs/cli/src/cli.ts
11721
11817
  init_src();
11722
- import { randomUUID as randomUUID4 } from "crypto";
11818
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
11723
11819
  import { lstat, readFile as readFile8, readdir as readdir2, stat as stat3, writeFile as writeFile5 } from "fs/promises";
11724
11820
  import path6 from "path";
11725
11821
 
@@ -12508,6 +12604,42 @@ class SandblocksClient {
12508
12604
  cancelAgentSession(projectId, sandboxId, sessionId) {
12509
12605
  return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" });
12510
12606
  }
12607
+ listWorkers(organizationId) {
12608
+ return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers`);
12609
+ }
12610
+ createWorker(input) {
12611
+ return this.request(`/v1/organizations/${encodeURIComponent(input.organizationId)}/workers`, {
12612
+ method: "POST",
12613
+ body: JSON.stringify(input)
12614
+ });
12615
+ }
12616
+ moveWorkerDeployments(organizationId, workerId, targetWorkerIds) {
12617
+ return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/move`, { method: "POST", body: JSON.stringify({ targetWorkerIds }) });
12618
+ }
12619
+ rotateWorkerSecret(organizationId, workerId) {
12620
+ return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/rotate-credential`, { method: "POST" });
12621
+ }
12622
+ approveWorkerUpdate(organizationId, workerId, version) {
12623
+ return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/approve-update`, {
12624
+ method: "POST",
12625
+ body: JSON.stringify({ version })
12626
+ });
12627
+ }
12628
+ listRegistries(organizationId) {
12629
+ return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/registries`);
12630
+ }
12631
+ createRegistry(input) {
12632
+ return this.request(`/v1/organizations/${encodeURIComponent(input.organizationId)}/registries`, {
12633
+ method: "POST",
12634
+ body: JSON.stringify(input)
12635
+ });
12636
+ }
12637
+ assignProjectRegistry(projectId, input) {
12638
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/registries`, {
12639
+ method: "PUT",
12640
+ body: JSON.stringify(input)
12641
+ });
12642
+ }
12511
12643
  listHosts(projectId) {
12512
12644
  return this.request(`/v1/projects/${encodeURIComponent(projectId)}/hosts`);
12513
12645
  }
@@ -12635,7 +12767,12 @@ class SandblocksClient {
12635
12767
  const image = String(operation.result?.image ?? "");
12636
12768
  if (!/@sha256:[a-f0-9]{64}$/.test(image))
12637
12769
  throw new Error("Published artifact digest is invalid");
12638
- return { image, workspaceId: input.workspaceId, deploymentId: input.deploymentId, bytes: input.bundle.byteLength };
12770
+ return {
12771
+ image,
12772
+ workspaceId: input.workspaceId,
12773
+ deploymentId: input.deploymentId,
12774
+ bytes: input.bundle.byteLength
12775
+ };
12639
12776
  }
12640
12777
  async deployPrebuiltArtifact(input) {
12641
12778
  const { operation } = await this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/operations`, {
@@ -12962,10 +13099,14 @@ function option(options, key) {
12962
13099
  // packages/libs/cli/src/sandbox.ts
12963
13100
  init_src();
12964
13101
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
13102
+ import { watch as watchFiles } from "fs";
12965
13103
  import { chmod as chmod3, mkdir as mkdir6, readFile as readFile5, rename, rm as rm4 } from "fs/promises";
12966
13104
  import path5 from "path";
12967
13105
  var COMMANDS = new Set([
12968
13106
  "up",
13107
+ "dev",
13108
+ "sync",
13109
+ "check",
12969
13110
  "create",
12970
13111
  "status",
12971
13112
  "deploy",
@@ -12982,6 +13123,12 @@ function isStatefulSandboxCommand(command) {
12982
13123
  async function runStatefulSandboxCommand(command, args, dependencies) {
12983
13124
  if (command === "up")
12984
13125
  return up(args, dependencies);
13126
+ if (command === "dev")
13127
+ return dev(args, dependencies);
13128
+ if (command === "sync")
13129
+ return sync(args, dependencies);
13130
+ if (command === "check")
13131
+ return check(args, dependencies);
12985
13132
  if (command === "create")
12986
13133
  return create(args, dependencies);
12987
13134
  if (command === "status")
@@ -13013,6 +13160,98 @@ async function up(args, dependencies) {
13013
13160
  }
13014
13161
  });
13015
13162
  }
13163
+ async function dev(args, dependencies) {
13164
+ await up(withDefaultOption(withDefaultEnvironment(args, "develop"), "pool", "development-fleet"), dependencies);
13165
+ }
13166
+ async function sync(args, dependencies) {
13167
+ const effectiveArgs = withDefaultEnvironment(args, "develop");
13168
+ const context = await getContext(effectiveArgs, dependencies);
13169
+ if (context.state.runtimeMode !== "development") {
13170
+ throw new Error("sandbox sync requires a development environment");
13171
+ }
13172
+ let previousDigest = "";
13173
+ const apply = async () => {
13174
+ const source = await dependencies.source(context.root, context.state.sourceManifest);
13175
+ const digest = createHash2("sha256").update(source.bundle).digest("hex");
13176
+ if (digest === previousDigest)
13177
+ return;
13178
+ previousDigest = digest;
13179
+ context.state.sequence += 1;
13180
+ const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}/sync`, {
13181
+ method: "POST",
13182
+ headers: {
13183
+ "content-type": "application/x-tar",
13184
+ "content-length": String(source.bundle.byteLength),
13185
+ "x-sandblocks-api-key": context.apiKey,
13186
+ "x-sandblocks-sandbox-id": context.state.sandboxId,
13187
+ ...context.state.sourceRevision ? { "x-sandblocks-base-revision": context.state.sourceRevision } : {},
13188
+ "idempotency-key": `sync:${context.state.sandboxId}:${context.state.sequence}:${digest}`
13189
+ },
13190
+ body: new Blob([Uint8Array.from(source.bundle)])
13191
+ });
13192
+ const submitted = await response.json().catch(() => ({}));
13193
+ if (!response.ok)
13194
+ throw new Error(String(submitted.error ?? `source sync failed (${response.status})`));
13195
+ const completed = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 5 * 60000);
13196
+ context.state.sourceRevision = String(completed.result?.revision ?? digest);
13197
+ context.state.sourceManifest = source.manifest;
13198
+ await writeState(context.file, context.state);
13199
+ console.log(`sync revision=${context.state.sourceRevision} changed=${completed.result?.changed ?? 0} deleted=${completed.result?.deleted ?? 0}`);
13200
+ if (context.options.checks !== "none") {
13201
+ const changedPaths = [
13202
+ ...Array.isArray(completed.result?.changedPaths) ? completed.result.changedPaths : [],
13203
+ ...Array.isArray(completed.result?.deletedPaths) ? completed.result.deletedPaths : []
13204
+ ].filter((value) => typeof value === "string");
13205
+ await runDevelopmentChecks(context, dependencies, undefined, changedPaths);
13206
+ }
13207
+ };
13208
+ await apply();
13209
+ if (context.options.watch !== true)
13210
+ return;
13211
+ console.log("watch waiting for source changes (Ctrl-C keeps the sandbox running)");
13212
+ while (true) {
13213
+ await waitForSourceChange(context.root);
13214
+ await apply();
13215
+ }
13216
+ }
13217
+ async function check(args, dependencies) {
13218
+ const context = await getContext(withDefaultEnvironment(args, "develop"), dependencies);
13219
+ await runDevelopmentChecks(context, dependencies, dependencies.option(context.options, "profile"));
13220
+ }
13221
+ async function waitForSourceChange(root) {
13222
+ await new Promise((resolveChange) => {
13223
+ let settled = false;
13224
+ let debounce;
13225
+ const watcher = watchFiles(root, { recursive: true }, (_event, filename) => {
13226
+ const path6 = String(filename ?? "").replaceAll("\\", "/");
13227
+ if (!path6 || path6 === ".git" || path6.startsWith(".git/") || path6 === ".sandblocks" || path6.startsWith(".sandblocks/") || path6 === "node_modules" || path6.includes("/node_modules/")) {
13228
+ return;
13229
+ }
13230
+ if (debounce)
13231
+ clearTimeout(debounce);
13232
+ debounce = setTimeout(() => {
13233
+ if (settled)
13234
+ return;
13235
+ settled = true;
13236
+ watcher.close();
13237
+ resolveChange();
13238
+ }, 150);
13239
+ });
13240
+ watcher.on("error", () => {
13241
+ if (settled)
13242
+ return;
13243
+ settled = true;
13244
+ watcher.close();
13245
+ setTimeout(resolveChange, 1000);
13246
+ });
13247
+ });
13248
+ }
13249
+ function withDefaultEnvironment(args, environment) {
13250
+ return withDefaultOption(args, "environment", environment);
13251
+ }
13252
+ function withDefaultOption(args, option2, value) {
13253
+ return args.some((candidate) => candidate === `--${option2}` || candidate.startsWith(`--${option2}=`)) ? args : [...args, `--${option2}`, value];
13254
+ }
13016
13255
  async function create(args, dependencies) {
13017
13256
  const options = dependencies.parse(args);
13018
13257
  const root = path5.resolve(options.positionals[0] ?? process.cwd());
@@ -13053,9 +13292,10 @@ async function create(args, dependencies) {
13053
13292
  source: dependencies.source,
13054
13293
  ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
13055
13294
  });
13056
- const hostId = String(imported.operation.assignedHostId ?? "");
13057
- if (!hostId)
13058
- throw new Error("workspace import did not bind a sandbox host");
13295
+ const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
13296
+ const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
13297
+ if (!workerId)
13298
+ throw new Error("workspace import did not bind a worker");
13059
13299
  const state = {
13060
13300
  version: 1,
13061
13301
  environment,
@@ -13065,14 +13305,17 @@ async function create(args, dependencies) {
13065
13305
  repository,
13066
13306
  leaseId: lease.lease.id,
13067
13307
  leaseToken: lease.token,
13068
- hostId,
13308
+ ...hostId ? { hostId } : {},
13309
+ workerId,
13310
+ sourceRevision: typeof imported.operation.result?.sha === "string" ? imported.operation.result.sha : undefined,
13311
+ sourceManifest: imported.manifest,
13069
13312
  sequence: 0,
13070
13313
  createdAt: new Date().toISOString()
13071
13314
  };
13072
13315
  await writeState(file, state);
13073
13316
  console.log(`sandbox ${sandboxId}`);
13074
13317
  console.log(`workspace ${workspaceId}`);
13075
- console.log(`host ${hostId}`);
13318
+ console.log(`worker ${workerId}`);
13076
13319
  } catch (error) {
13077
13320
  await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
13078
13321
  method: "DELETE"
@@ -13095,8 +13338,14 @@ async function status2(args, dependencies) {
13095
13338
  console.log(`state ${path5.relative(context.root, context.file)}`);
13096
13339
  console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
13097
13340
  console.log(`workspace ${context.state.workspaceId}`);
13098
- console.log(`host ${context.state.hostId}`);
13341
+ console.log(`worker ${context.state.workerId}`);
13099
13342
  console.log(`lease expires ${session.session.expiresAt}`);
13343
+ if (context.state.runtimeMode)
13344
+ console.log(`mode ${context.state.runtimeMode}`);
13345
+ if (context.state.sourceRevision)
13346
+ console.log(`revision ${context.state.sourceRevision}`);
13347
+ if (context.state.quality)
13348
+ console.log(`quality ${context.state.quality.status} (${context.state.quality.sourceRevision})`);
13100
13349
  for (const route of context.state.previewUrls ?? [])
13101
13350
  console.log(`${route.service ?? "service"} ${route.url}`);
13102
13351
  }
@@ -13125,15 +13374,22 @@ async function withDeploymentLock(args, dependencies, action) {
13125
13374
  }
13126
13375
  async function deployUnlocked(args, dependencies) {
13127
13376
  const context = await getContext(args, dependencies);
13128
- if (context.state.deploymentId)
13377
+ const resume = context.options.resume === true;
13378
+ if (context.state.deploymentId && !(resume && context.state.candidate?.deploymentId === context.state.deploymentId))
13129
13379
  throw new Error("preview is already deployed; use sandbox redeploy");
13130
13380
  const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
13131
13381
  const stack = loaded.manifest.name ?? context.state.repository;
13132
13382
  const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
13383
+ await assertEnvironmentBranch(context.root, preview);
13384
+ context.state.managedEnvironment = preview.managedEnvironment;
13385
+ if (preview.mode === "development") {
13386
+ await deployDevelopmentUnlocked(context, loaded, preview);
13387
+ return;
13388
+ }
13133
13389
  const deploymentTarget = await resolveDeploymentTarget(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
13134
13390
  const targetHost = deploymentTarget.targetHost;
13135
13391
  context.state.deploymentHostId = deploymentTarget.hostId;
13136
- const resume = context.options.resume === true;
13392
+ context.state.deploymentWorkerId = deploymentTarget.workerId;
13137
13393
  const previousCandidate = resume ? context.state.candidate : undefined;
13138
13394
  if (resume && !previousCandidate)
13139
13395
  throw new Error("no resumable candidate exists for this sandbox");
@@ -13143,13 +13399,14 @@ async function deployUnlocked(args, dependencies) {
13143
13399
  throw new Error("deployment ID must be a UUID");
13144
13400
  if (previousCandidate && requestedDeploymentId && requestedDeploymentId !== previousCandidate.deploymentId)
13145
13401
  throw new Error("--deployment-id does not match the resumable candidate");
13146
- const environmentSnapshotId = previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
13147
- context.state.candidate = previousCandidate ?? { deploymentId, environmentSnapshotId };
13402
+ const environmentSnapshotId = resume ? await createEnvironmentSnapshot(context) : previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
13403
+ context.state.candidate = previousCandidate ? { ...previousCandidate, environmentSnapshotId } : { deploymentId, environmentSnapshotId };
13148
13404
  await writeState(context.file, context.state);
13149
13405
  const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
13150
13406
  const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
13151
13407
  let buildWorkspaceId = previousCandidate?.buildWorkspaceId;
13152
13408
  let buildHostId = previousCandidate?.buildHostId;
13409
+ let buildWorkerId = previousCandidate?.buildWorkerId;
13153
13410
  const suppliedArtifact = dependencies.option(context.options, "artifact");
13154
13411
  if (suppliedArtifact && !/@sha256:[a-f0-9]{64}$/.test(suppliedArtifact))
13155
13412
  throw new Error("--artifact must be an immutable OCI image digest");
@@ -13173,10 +13430,17 @@ async function deployUnlocked(args, dependencies) {
13173
13430
  source: dependencies.source,
13174
13431
  ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
13175
13432
  });
13176
- buildHostId = String(imported.operation.assignedHostId ?? "");
13177
- if (!buildHostId)
13178
- throw new Error("build workspace did not bind a build host");
13179
- context.state.candidate = { deploymentId, environmentSnapshotId, buildWorkspaceId, buildHostId };
13433
+ buildHostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
13434
+ buildWorkerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
13435
+ if (!buildWorkerId)
13436
+ throw new Error("build workspace did not bind a build worker");
13437
+ context.state.candidate = {
13438
+ deploymentId,
13439
+ environmentSnapshotId,
13440
+ buildWorkspaceId,
13441
+ buildHostId,
13442
+ buildWorkerId
13443
+ };
13180
13444
  await writeState(context.file, context.state);
13181
13445
  const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13182
13446
  method: "POST",
@@ -13184,7 +13448,7 @@ async function deployUnlocked(args, dependencies) {
13184
13448
  body: JSON.stringify({
13185
13449
  kind: "workspace.step.run",
13186
13450
  stack: context.state.repository,
13187
- environmentId: context.state.environment,
13451
+ environmentId: managedEnvironmentId(context),
13188
13452
  environmentSnapshotId,
13189
13453
  payload: {
13190
13454
  workspaceId: buildWorkspaceId,
@@ -13194,7 +13458,11 @@ async function deployUnlocked(args, dependencies) {
13194
13458
  serviceUrls,
13195
13459
  steps: serviceSteps,
13196
13460
  ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
13197
- placement: { hostId: buildHostId, memoryReservationBytes: 2 * 1024 * 1024 * 1024 }
13461
+ placement: {
13462
+ workerId: buildWorkerId,
13463
+ ...buildHostId ? { hostId: buildHostId } : {},
13464
+ memoryReservationBytes: 2 * 1024 * 1024 * 1024
13465
+ }
13198
13466
  },
13199
13467
  maxAttempts: 1
13200
13468
  })
@@ -13205,22 +13473,29 @@ async function deployUnlocked(args, dependencies) {
13205
13473
  environmentSnapshotId,
13206
13474
  buildWorkspaceId,
13207
13475
  buildHostId,
13476
+ buildWorkerId,
13208
13477
  buildCompleted: true
13209
13478
  };
13210
13479
  await writeState(context.file, context.state);
13211
13480
  }
13212
- if (!buildHostId)
13213
- throw new Error("resumable build host is unavailable");
13481
+ if (!buildWorkerId)
13482
+ throw new Error("resumable build worker is unavailable");
13214
13483
  const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13215
13484
  method: "POST",
13216
- headers: { "idempotency-key": `preview:${deploymentId}:artifact` },
13485
+ headers: {
13486
+ "idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
13487
+ },
13217
13488
  body: JSON.stringify({
13218
13489
  kind: "workspace.artifact.publish",
13219
13490
  payload: {
13220
13491
  workspaceId: buildWorkspaceId,
13221
13492
  deploymentId,
13222
13493
  stack,
13223
- placement: { hostId: buildHostId, memoryReservationBytes: 1024 * 1024 * 1024 }
13494
+ placement: {
13495
+ workerId: buildWorkerId,
13496
+ ...buildHostId ? { hostId: buildHostId } : {},
13497
+ memoryReservationBytes: 1024 * 1024 * 1024
13498
+ }
13224
13499
  },
13225
13500
  maxAttempts: 1
13226
13501
  })
@@ -13234,6 +13509,7 @@ async function deployUnlocked(args, dependencies) {
13234
13509
  environmentSnapshotId,
13235
13510
  buildWorkspaceId,
13236
13511
  buildHostId,
13512
+ buildWorkerId,
13237
13513
  buildCompleted: true,
13238
13514
  artifactImage
13239
13515
  };
@@ -13257,7 +13533,9 @@ async function deployUnlocked(args, dependencies) {
13257
13533
  if (!artifactImage) {
13258
13534
  const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13259
13535
  method: "POST",
13260
- headers: { "idempotency-key": `preview:${deploymentId}:artifact` },
13536
+ headers: {
13537
+ "idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
13538
+ },
13261
13539
  body: JSON.stringify({
13262
13540
  kind: "workspace.artifact.publish",
13263
13541
  payload: {
@@ -13265,7 +13543,8 @@ async function deployUnlocked(args, dependencies) {
13265
13543
  deploymentId,
13266
13544
  stack,
13267
13545
  placement: {
13268
- hostId: context.state.hostId,
13546
+ workerId: context.state.workerId,
13547
+ ...context.state.hostId ? { hostId: context.state.hostId } : {},
13269
13548
  memoryReservationBytes: 1024 * 1024 * 1024
13270
13549
  }
13271
13550
  },
@@ -13282,12 +13561,12 @@ async function deployUnlocked(args, dependencies) {
13282
13561
  const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13283
13562
  method: "POST",
13284
13563
  headers: {
13285
- "idempotency-key": `preview:${deploymentId}:deploy`
13564
+ "idempotency-key": resume ? `preview:${deploymentId}:deploy:resume:${randomUUID2()}` : `preview:${deploymentId}:deploy`
13286
13565
  },
13287
13566
  body: JSON.stringify({
13288
13567
  kind: "workspace.service.deploy",
13289
13568
  stack: context.state.repository,
13290
- environmentId: context.state.environment,
13569
+ environmentId: managedEnvironmentId(context),
13291
13570
  environmentSnapshotId,
13292
13571
  payload: {
13293
13572
  workspaceId: context.state.workspaceId,
@@ -13301,7 +13580,8 @@ async function deployUnlocked(args, dependencies) {
13301
13580
  serviceUrls,
13302
13581
  services: preview.services,
13303
13582
  placement: {
13304
- hostId: deploymentTarget.hostId,
13583
+ workerId: deploymentTarget.workerId,
13584
+ ...deploymentTarget.hostId ? { hostId: deploymentTarget.hostId } : {},
13305
13585
  requiredWorkerRole: "runtime",
13306
13586
  pool: deploymentTarget.pool,
13307
13587
  memoryReservationBytes: serviceMemoryReservation(preview.services)
@@ -13313,7 +13593,7 @@ async function deployUnlocked(args, dependencies) {
13313
13593
  operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
13314
13594
  } catch (error) {
13315
13595
  const message = error instanceof Error ? error.message : String(error);
13316
- throw new Error(`${message}; retry with ` + "`sandblocks sandbox deploy . --resume`");
13596
+ throw new Error(`${message}; retry with \`sandblocks sandbox deploy . --resume\``);
13317
13597
  }
13318
13598
  context.state.deploymentId = deploymentId;
13319
13599
  await writeState(context.file, context.state);
@@ -13334,15 +13614,15 @@ async function deployUnlocked(args, dependencies) {
13334
13614
  route.url
13335
13615
  ]
13336
13616
  ] : []));
13337
- const check = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13617
+ const check2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13338
13618
  method: "POST",
13339
13619
  headers: {
13340
- "idempotency-key": `preview:${deploymentId}:checks`
13620
+ "idempotency-key": resume ? `preview:${deploymentId}:checks:resume:${randomUUID2()}` : `preview:${deploymentId}:checks`
13341
13621
  },
13342
13622
  body: JSON.stringify({
13343
13623
  kind: "workspace.check.run",
13344
13624
  stack: context.state.repository,
13345
- environmentId: context.state.environment,
13625
+ environmentId: managedEnvironmentId(context),
13346
13626
  environmentSnapshotId,
13347
13627
  payload: {
13348
13628
  workspaceId: buildWorkspaceId ?? context.state.workspaceId,
@@ -13351,12 +13631,15 @@ async function deployUnlocked(args, dependencies) {
13351
13631
  runtimeType: preview.deploymentType,
13352
13632
  checks: preview.steps,
13353
13633
  serviceUrls: serviceUrls2,
13354
- placement: { hostId: buildHostId ?? context.state.hostId }
13634
+ placement: {
13635
+ workerId: buildWorkerId ?? context.state.workerId,
13636
+ ...buildHostId ?? context.state.hostId ? { hostId: buildHostId ?? context.state.hostId } : {}
13637
+ }
13355
13638
  },
13356
13639
  maxAttempts: 1
13357
13640
  })
13358
13641
  });
13359
- const checked2 = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
13642
+ const checked2 = await wait(context.apiUrl, context.apiKey, check2.operation.id, 60 * 60000);
13360
13643
  checkEvidence = checked2.result?.checks ?? [];
13361
13644
  if (checked2.result?.passed !== true) {
13362
13645
  throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
@@ -13367,13 +13650,6 @@ async function deployUnlocked(args, dependencies) {
13367
13650
  await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
13368
13651
  return;
13369
13652
  });
13370
- if (buildWorkspaceId)
13371
- await destroyDetachedWorkspace(context, buildWorkspaceId, `preview:${deploymentId}:build-cleanup`).catch(() => {
13372
- return;
13373
- });
13374
- await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
13375
- return;
13376
- });
13377
13653
  throw error;
13378
13654
  }
13379
13655
  if (buildWorkspaceId)
@@ -13388,6 +13664,219 @@ async function deployUnlocked(args, dependencies) {
13388
13664
  for (const route of previewUrls)
13389
13665
  console.log(`${route.service ?? "service"} ${route.url}`);
13390
13666
  }
13667
+ async function deployDevelopmentUnlocked(context, loaded, preview) {
13668
+ if (context.state.deploymentId)
13669
+ throw new Error("development sandbox is already running; use sandbox sync");
13670
+ const stack = loaded.manifest.name ?? context.state.repository;
13671
+ const target = await resolveDevelopmentTarget(context, preview.targetHost);
13672
+ const deploymentId = randomUUID2();
13673
+ const environmentSnapshotId = await createEnvironmentSnapshot(context);
13674
+ context.state.deploymentId = deploymentId;
13675
+ context.state.deploymentHostId = target.hostId;
13676
+ context.state.deploymentWorkerId = target.workerId;
13677
+ context.state.runtimeMode = "development";
13678
+ await writeState(context.file, context.state);
13679
+ try {
13680
+ const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
13681
+ const steps = resolveServiceSteps(loaded.manifest, context.state.environment);
13682
+ if (steps.length) {
13683
+ const submitted2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13684
+ method: "POST",
13685
+ headers: { "idempotency-key": `development:${deploymentId}:setup` },
13686
+ body: JSON.stringify({
13687
+ kind: "workspace.step.run",
13688
+ stack,
13689
+ environmentId: managedEnvironmentId(context),
13690
+ environmentSnapshotId,
13691
+ payload: {
13692
+ workspaceId: context.state.workspaceId,
13693
+ sandboxId: context.state.sandboxId,
13694
+ revisionId: context.state.sourceRevision ?? deploymentId,
13695
+ runtimeType: preview.deploymentType,
13696
+ serviceUrls,
13697
+ steps,
13698
+ ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
13699
+ placement: {
13700
+ workerId: context.state.workerId,
13701
+ ...context.state.hostId ? { hostId: context.state.hostId } : {},
13702
+ requiredWorkerRole: "development"
13703
+ }
13704
+ },
13705
+ maxAttempts: 1
13706
+ })
13707
+ });
13708
+ await wait(context.apiUrl, context.apiKey, submitted2.operation.id, 30 * 60000);
13709
+ }
13710
+ const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13711
+ method: "POST",
13712
+ headers: { "idempotency-key": `development:${deploymentId}:services` },
13713
+ body: JSON.stringify({
13714
+ kind: "workspace.service.deploy",
13715
+ stack,
13716
+ environmentId: managedEnvironmentId(context),
13717
+ environmentSnapshotId,
13718
+ payload: {
13719
+ workspaceId: context.state.workspaceId,
13720
+ deploymentId,
13721
+ sandboxId: context.state.sandboxId,
13722
+ runtimeType: preview.deploymentType,
13723
+ stack,
13724
+ workspace: preview.workspace,
13725
+ mutableDevelopment: true,
13726
+ sourceRevision: context.state.sourceRevision ?? deploymentId,
13727
+ targetHost: target.targetHost,
13728
+ serviceUrls,
13729
+ services: preview.services,
13730
+ placement: {
13731
+ workerId: context.state.workerId,
13732
+ ...context.state.hostId ? { hostId: context.state.hostId } : {},
13733
+ requiredWorkerRole: "development",
13734
+ memoryReservationBytes: serviceMemoryReservation(preview.services)
13735
+ }
13736
+ },
13737
+ maxAttempts: 1
13738
+ })
13739
+ });
13740
+ const operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
13741
+ if (typeof preview.ssoEnabled === "boolean") {
13742
+ await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`, { method: "PUT", body: JSON.stringify({ ssoEnabled: preview.ssoEnabled }) });
13743
+ }
13744
+ const sandbox = await recordSandbox(context, loaded, preview, operation.result?.serviceRoutes ?? [], operation.result?.healthEvidence ?? [], operation.result?.images ?? [], "available");
13745
+ const previewUrls = sandbox.previewUrls ?? [];
13746
+ context.state.previewUrls = previewUrls;
13747
+ await writeState(context.file, context.state);
13748
+ console.log(`development ${deploymentId} available`);
13749
+ for (const route of previewUrls)
13750
+ console.log(`${route.service ?? "service"} ${route.url}`);
13751
+ await runDevelopmentChecks(context, {
13752
+ parse: () => context.options,
13753
+ option: (options, key) => typeof options[key] === "string" ? options[key] : undefined,
13754
+ load: async () => loaded,
13755
+ source: async () => ({ bundle: new Uint8Array, files: 0 })
13756
+ });
13757
+ } catch (error) {
13758
+ context.state.runtimeMode = "development";
13759
+ await writeState(context.file, context.state);
13760
+ throw error;
13761
+ } finally {
13762
+ await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
13763
+ return;
13764
+ });
13765
+ }
13766
+ }
13767
+ async function runDevelopmentChecks(context, dependencies, requestedProfile, changedPaths = []) {
13768
+ if (!context.state.deploymentId || !context.state.previewUrls?.length) {
13769
+ throw new Error("development services must be available before checks run");
13770
+ }
13771
+ const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
13772
+ const environment = resolveEnvironment(loaded.manifest, context.state.environment);
13773
+ if (environment.mode !== "development")
13774
+ throw new Error("continuous checks require a development environment");
13775
+ const profile = requestedProfile ?? environment.checkProfile;
13776
+ if (!["none", "smoke", "full"].includes(profile))
13777
+ throw new Error("check profile must be none, smoke, or full");
13778
+ const sourceRevision = context.state.sourceRevision ?? context.state.deploymentId;
13779
+ if (profile === "none") {
13780
+ context.state.quality = { sourceRevision, status: "unchecked", checks: [] };
13781
+ await writeState(context.file, context.state);
13782
+ console.log(`quality unchecked revision=${sourceRevision}`);
13783
+ return;
13784
+ }
13785
+ const profileChecks = environment.steps.filter((candidate) => profile === "full" || candidate.profile === "smoke");
13786
+ const checks = profile === "full" || !changedPaths.length ? profileChecks : profileChecks.filter((candidate) => !candidate.inputs.length || candidate.inputs.some((input) => changedPaths.some((path6) => repositoryPatternMatches(input, path6))));
13787
+ if (!profileChecks.length)
13788
+ throw new Error(`environment '${environment.id}' has no ${profile} checks`);
13789
+ if (!checks.length) {
13790
+ context.state.quality = {
13791
+ sourceRevision,
13792
+ status: context.state.quality?.status === "failed" ? "failed" : "passed",
13793
+ checks: context.state.quality?.checks ?? []
13794
+ };
13795
+ await writeState(context.file, context.state);
13796
+ console.log(`quality reused revision=${sourceRevision} (no affected application suites)`);
13797
+ return;
13798
+ }
13799
+ await waitForPublishedRoutes(context.state.previewUrls);
13800
+ const serviceUrls = Object.fromEntries(context.state.previewUrls.flatMap((route) => route.service ? [[`SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`, route.url]] : []));
13801
+ context.state.sequence += 1;
13802
+ const snapshotId = await createEnvironmentSnapshot(context);
13803
+ try {
13804
+ const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13805
+ method: "POST",
13806
+ headers: {
13807
+ "idempotency-key": `development:${context.state.deploymentId}:checks:${sourceRevision}:${profile}:${context.state.sequence}`
13808
+ },
13809
+ body: JSON.stringify({
13810
+ kind: "workspace.check.run",
13811
+ stack: loaded.manifest.name ?? context.state.repository,
13812
+ environmentId: managedEnvironmentId(context),
13813
+ environmentSnapshotId: snapshotId,
13814
+ payload: {
13815
+ workspaceId: context.state.workspaceId,
13816
+ deploymentId: context.state.deploymentId,
13817
+ sandboxId: context.state.sandboxId,
13818
+ runtimeType: environment.deploymentType,
13819
+ sourceRevision,
13820
+ checks,
13821
+ serviceUrls,
13822
+ placement: {
13823
+ workerId: context.state.workerId,
13824
+ ...context.state.hostId ? { hostId: context.state.hostId } : {},
13825
+ requiredWorkerRole: "development"
13826
+ }
13827
+ },
13828
+ maxAttempts: 1
13829
+ })
13830
+ });
13831
+ const completed = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 60 * 60000);
13832
+ const checksResult = Array.isArray(completed.result?.checks) ? completed.result.checks : [];
13833
+ const selectedIds = new Set(checks.map((item) => item.id));
13834
+ const reused = (context.state.quality?.checks ?? []).filter((item) => item && !selectedIds.has(String(item.check ?? ""))).map((item) => ({
13835
+ ...item,
13836
+ sourceRevision,
13837
+ status: item.status === "passed" || item.status === "reused" ? "reused" : item.status,
13838
+ reusedFromRevision: item.sourceRevision
13839
+ }));
13840
+ const combined = [...checksResult, ...reused];
13841
+ const status3 = completed.result?.passed === true && combined.every((item) => item.required === false || ["passed", "reused"].includes(item.status)) ? "passed" : "failed";
13842
+ context.state.quality = { sourceRevision, status: status3, checks: combined };
13843
+ await writeState(context.file, context.state);
13844
+ console.log(`quality ${status3} revision=${sourceRevision}`);
13845
+ for (const result of checksResult) {
13846
+ const item = result;
13847
+ console.log(` ${String(item.application ?? item.check ?? "check").padEnd(12)} ${item.status} ${item.durationMs ?? 0}ms`);
13848
+ }
13849
+ } finally {
13850
+ await releaseEnvironmentSnapshot(context, snapshotId).catch(() => {
13851
+ return;
13852
+ });
13853
+ }
13854
+ }
13855
+ async function assertEnvironmentBranch(root, environment) {
13856
+ if (!environment.branches.length)
13857
+ return;
13858
+ const result = Bun.spawnSync(["git", "branch", "--show-current"], { cwd: root });
13859
+ const branch = result.exitCode === 0 ? result.stdout.toString().trim() : "";
13860
+ if (!branch)
13861
+ throw new Error(`environment '${environment.id}' requires a named Git branch`);
13862
+ const allowed = environment.branches.some((pattern) => {
13863
+ const expression = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
13864
+ return expression.test(branch);
13865
+ });
13866
+ if (!allowed) {
13867
+ throw new Error(`branch '${branch}' cannot deploy to environment '${environment.id}' (allowed: ${environment.branches.join(", ")})`);
13868
+ }
13869
+ }
13870
+ function escapeRegExp(value) {
13871
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
13872
+ }
13873
+ function repositoryPatternMatches(pattern, path6) {
13874
+ const normalized = pattern.replace(/^\.\//, "").replace(/\/\*\*$/, "").replace(/\/$/, "");
13875
+ if (!normalized.includes("*"))
13876
+ return path6 === normalized || path6.startsWith(`${normalized}/`);
13877
+ const expression = new RegExp(`^${normalized.split("**").map((part) => part.split("*").map(escapeRegExp).join("[^/]*")).join(".*")}(?:/.*)?$`);
13878
+ return expression.test(path6);
13879
+ }
13391
13880
  async function redeploy(args, dependencies) {
13392
13881
  const context = await getContext(args, dependencies);
13393
13882
  const routing = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-aliases`);
@@ -13415,10 +13904,12 @@ async function redeploy(args, dependencies) {
13415
13904
  source: dependencies.source,
13416
13905
  ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
13417
13906
  });
13418
- const hostId = String(imported.operation.assignedHostId ?? "");
13419
- if (!hostId)
13420
- throw new Error("workspace re-import did not bind a sandbox host");
13907
+ const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
13908
+ const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
13909
+ if (!workerId)
13910
+ throw new Error("workspace re-import did not bind a worker");
13421
13911
  context.state.hostId = hostId;
13912
+ context.state.workerId = workerId;
13422
13913
  await writeState(context.file, context.state);
13423
13914
  await deploy(args, dependencies);
13424
13915
  if (previousDeploymentId)
@@ -13494,25 +13985,47 @@ async function down(args, dependencies) {
13494
13985
  await rm4(context.file, { force: true });
13495
13986
  console.log(`sandbox ${context.state.sandboxId} destroyed`);
13496
13987
  }
13988
+ async function resolveDevelopmentTarget(context, fallback) {
13989
+ const project = encodeURIComponent(context.state.projectId);
13990
+ const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
13991
+ const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
13992
+ const worker = workers.find((candidate) => candidate?.id === context.state.workerId);
13993
+ if (!worker || worker.status !== "online")
13994
+ throw new Error("development workspace worker is offline");
13995
+ const configuredTarget = worker?.configuration?.targetAddress;
13996
+ const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
13997
+ if (!targetHost)
13998
+ throw new Error("development worker targetAddress or environment targetHost is required");
13999
+ return {
14000
+ workerId: String(worker.id),
14001
+ ...worker.hostId ? { hostId: String(worker.hostId) } : {},
14002
+ targetHost
14003
+ };
14004
+ }
13497
14005
  async function resolveDeploymentTarget(context, fallback) {
13498
14006
  const project = encodeURIComponent(context.state.projectId);
13499
- const [hostBody, workerBody] = await Promise.all([
13500
- request(context.apiUrl, context.apiKey, `/v1/projects/${project}/hosts`),
13501
- request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`)
13502
- ]);
14007
+ const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
13503
14008
  const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
13504
14009
  const requestedPool = process.env.SANDBLOCKS_DEPLOYMENT_WORKER_POOL?.trim() || process.env.SANDBLOCKS_RUNTIME_WORKER_POOL?.trim();
13505
14010
  const eligible = workers.filter((worker2) => worker2?.status === "online" && Array.isArray(worker2.roles) && worker2.roles.includes("runtime"));
13506
- const worker = eligible.find((candidate) => requestedPool && candidate.pool === requestedPool) ?? eligible[0];
13507
- if (!worker?.hostId)
14011
+ const worker = eligible.find((candidate) => typeof fallback === "string" && candidate?.configuration?.targetAddress === fallback.trim()) ?? eligible.find((candidate) => requestedPool && candidate.pool === requestedPool) ?? eligible[0];
14012
+ if (!worker?.id)
13508
14013
  throw new Error("no online deployment worker is available");
13509
- const hosts = Array.isArray(hostBody.hosts) ? hostBody.hosts : [];
13510
- const host = hosts.find((candidate) => candidate?.id === worker.hostId);
13511
- const configuredTarget = worker?.configuration?.targetAddress ?? host?.labels?.targetHost;
14014
+ let configuredTarget = worker?.configuration?.targetAddress;
14015
+ if (!configuredTarget && worker.hostId) {
14016
+ const legacyHosts = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/hosts`);
14017
+ const host = Array.isArray(legacyHosts.hosts) ? legacyHosts.hosts.find((candidate) => candidate?.id === worker.hostId) : undefined;
14018
+ configuredTarget = host?.labels?.targetHost;
14019
+ }
13512
14020
  const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
13513
14021
  if (!targetHost)
13514
- throw new Error("deployment host targetHost label or preview targetHost is required");
13515
- return { hostId: String(worker.hostId), targetHost, pool: String(worker.pool ?? "unpooled") };
14022
+ throw new Error("deployment worker targetAddress or preview targetHost is required");
14023
+ return {
14024
+ workerId: String(worker.id),
14025
+ ...worker.hostId ? { hostId: String(worker.hostId) } : {},
14026
+ targetHost,
14027
+ pool: String(worker.pool ?? "unpooled")
14028
+ };
13516
14029
  }
13517
14030
  async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
13518
14031
  const deploymentId = context.state.deploymentId;
@@ -13553,7 +14066,7 @@ async function createEnvironmentSnapshot(context) {
13553
14066
  body: JSON.stringify({
13554
14067
  stack: context.state.repository,
13555
14068
  app: "*",
13556
- environmentId: context.state.environment
14069
+ environmentId: managedEnvironmentId(context)
13557
14070
  })
13558
14071
  });
13559
14072
  const id2 = body.snapshot?.id;
@@ -13562,6 +14075,9 @@ async function createEnvironmentSnapshot(context) {
13562
14075
  }
13563
14076
  return id2;
13564
14077
  }
14078
+ function managedEnvironmentId(context) {
14079
+ return context.state.managedEnvironment ?? context.state.environment;
14080
+ }
13565
14081
  async function releaseEnvironmentSnapshot(context, snapshotId) {
13566
14082
  await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
13567
14083
  }
@@ -13581,7 +14097,13 @@ async function destroyDeploymentById(context, deploymentId) {
13581
14097
  kind: "workspace.service.destroy",
13582
14098
  payload: {
13583
14099
  deploymentId,
13584
- placement: { hostId: context.state.deploymentHostId ?? context.state.hostId }
14100
+ placement: context.state.deploymentWorkerId ? {
14101
+ workerId: context.state.deploymentWorkerId,
14102
+ requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
14103
+ } : {
14104
+ hostId: context.state.deploymentHostId ?? context.state.hostId,
14105
+ requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
14106
+ }
13585
14107
  },
13586
14108
  maxAttempts: 1
13587
14109
  })
@@ -13629,6 +14151,7 @@ async function importSource(input) {
13629
14151
  return {
13630
14152
  files: source.files,
13631
14153
  bytes: source.bundle.byteLength,
14154
+ manifest: source.manifest,
13632
14155
  operation: await wait(input.apiUrl, input.apiKey, submitted.operation.id)
13633
14156
  };
13634
14157
  }
@@ -13651,6 +14174,10 @@ async function getContext(args, dependencies) {
13651
14174
  let state;
13652
14175
  try {
13653
14176
  state = JSON.parse(await readFile5(file, "utf8"));
14177
+ if (!state.workerId && state.hostId)
14178
+ state.workerId = state.hostId;
14179
+ if (!state.workerId)
14180
+ throw new Error("sandbox state has no bound worker");
13654
14181
  } catch {
13655
14182
  throw new Error(`sandbox '${environment}' does not exist; use sandbox create or up`);
13656
14183
  }
@@ -13674,8 +14201,10 @@ async function request(apiUrl, apiKey, pathname, init = {}) {
13674
14201
  headers.set("content-type", "application/json");
13675
14202
  const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
13676
14203
  const body = await response.json().catch(() => ({}));
13677
- if (!response.ok)
13678
- throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
14204
+ if (!response.ok) {
14205
+ const placement = body.placement ? `: ${JSON.stringify(body.placement)}` : "";
14206
+ throw new Error(`${String(body.error ?? `Sandblocks request failed (${response.status})`)}${placement}`);
14207
+ }
13679
14208
  return body;
13680
14209
  }
13681
14210
  async function leaseRequest(context, pathname, init = {}) {
@@ -13820,7 +14349,7 @@ function resolvedServiceDomains(sandboxId, deploymentId, preview) {
13820
14349
  ]));
13821
14350
  }
13822
14351
  function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
13823
- const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${deploymentId}/${service}`).digest("hex").slice(0, 8)}`;
14352
+ const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${service}`).digest("hex").slice(0, 8)}`;
13824
14353
  const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
13825
14354
  return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
13826
14355
  }
@@ -15120,7 +15649,16 @@ var HELP = `Sandblocks CLI
15120
15649
  sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
15121
15650
  [--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
15122
15651
  sandblocks artifact push <directory> --project <id> --stack <id> [--pool <pool>] [--json]
15652
+ sandblocks worker <list|create|rotate-secret|drain|resume|approve-update> --organization <id>
15653
+ [--worker <uuid>] [--name <name>] [--type <build|deployment>]
15654
+ [--pool-id <uuid>] [--project <id>] [--version <version>] [--json]
15655
+ sandblocks registry <list|create|assign> --organization <id> [--project <id>]
15656
+ [--name <name>] [--endpoint <url>] [--registry <uuid>]
15657
+ [--purpose <build-cache|artifact-publication|deployment-pull>] [--json]
15123
15658
  sandblocks sandbox up [directory] [--environment <id>] [--project <id>] [--build-pool <pool>]
15659
+ sandblocks sandbox dev [directory] [--environment <id>] [--checks <smoke|full|none>]
15660
+ sandblocks sandbox sync [directory] [--environment <id>] [--watch] [--checks <smoke|full|none>]
15661
+ sandblocks sandbox check [directory] [--environment <id>] [--profile <smoke|full>]
15124
15662
  sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
15125
15663
  sandblocks sandbox deploy [directory] [--environment <id>] [--build-pool <pool>]
15126
15664
  [--artifact <image@sha256:digest>] [--deployment-id <uuid>] [--resume]
@@ -15179,10 +15717,7 @@ async function run3(argv = process.argv.slice(2)) {
15179
15717
  parse: parse2,
15180
15718
  option: option2,
15181
15719
  load,
15182
- source: async (root) => {
15183
- const bundle = await createSourceTar(root);
15184
- return { bundle, files: sourceFileCount };
15185
- }
15720
+ source: createSourceTar
15186
15721
  })
15187
15722
  });
15188
15723
  else if (command === "runtime")
@@ -15193,6 +15728,10 @@ async function run3(argv = process.argv.slice(2)) {
15193
15728
  await secret(args);
15194
15729
  else if (command === "artifact")
15195
15730
  await artifact(args);
15731
+ else if (command === "worker")
15732
+ await worker(args);
15733
+ else if (command === "registry")
15734
+ await registry(args);
15196
15735
  else if (command === "sandbox")
15197
15736
  await sandbox(args);
15198
15737
  else if (command === "sdk")
@@ -15303,21 +15842,21 @@ async function doctor(args) {
15303
15842
  { label: "artifact publish", kind: "workspace.artifact.publish", role: "build", memory: 1024 ** 3 },
15304
15843
  { label: "service deploy", kind: "workspace.service.deploy", role: "runtime", memory: 6 * 1024 ** 3 }
15305
15844
  ];
15306
- const explanations = await Promise.all(checks.map(async (check) => {
15845
+ const explanations = await Promise.all(checks.map(async (check2) => {
15307
15846
  const query = new URLSearchParams({
15308
- kind: check.kind,
15309
- role: check.role,
15310
- memoryReservationBytes: String(check.memory)
15847
+ kind: check2.kind,
15848
+ role: check2.role,
15849
+ memoryReservationBytes: String(check2.memory)
15311
15850
  });
15312
15851
  const result = await sandblocksRequest(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/placement/explain?${query}`);
15313
- return { ...check, explanation: result.explanation };
15852
+ return { ...check2, explanation: result.explanation };
15314
15853
  }));
15315
15854
  if (options.json)
15316
15855
  console.log(JSON.stringify({ projectId, explanations }, null, 2));
15317
15856
  else
15318
- for (const check of explanations) {
15319
- const explanation = objectRecord(check.explanation);
15320
- console.log(`${check.label.padEnd(16)} ${explanation.eligible ? "eligible" : "blocked"}`);
15857
+ for (const check2 of explanations) {
15858
+ const explanation = objectRecord(check2.explanation);
15859
+ console.log(`${check2.label.padEnd(16)} ${explanation.eligible ? "eligible" : "blocked"}`);
15321
15860
  if (!explanation.eligible && Array.isArray(explanation.candidates))
15322
15861
  for (const candidate of explanation.candidates) {
15323
15862
  const row = objectRecord(candidate);
@@ -15479,6 +16018,102 @@ async function artifact(args) {
15479
16018
  console.log(`deploy with: sandblocks sandbox deploy . --artifact ${result.image} --deployment-id ${result.deploymentId}`);
15480
16019
  }
15481
16020
  }
16021
+ async function worker(args) {
16022
+ const [action = "list", ...rest] = args;
16023
+ const options = parse2(rest);
16024
+ const organizationId = option2(options, "organization");
16025
+ const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
16026
+ const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
16027
+ if (!organizationId)
16028
+ throw new Error("worker command requires --organization");
16029
+ if (!apiUrl || !apiKey)
16030
+ throw new Error("worker command requires Sandblocks API URL and key");
16031
+ const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
16032
+ let body;
16033
+ if (action === "list")
16034
+ body = await client2.listWorkers(organizationId);
16035
+ else if (action === "create") {
16036
+ const name = option2(options, "name");
16037
+ const type = option2(options, "type");
16038
+ if (!name || type !== "build" && type !== "deployment")
16039
+ throw new Error("worker create requires --name and --type build|deployment");
16040
+ body = await client2.createWorker({
16041
+ organizationId,
16042
+ name,
16043
+ type,
16044
+ ...option2(options, "pool-id") ? { poolId: option2(options, "pool-id") } : {},
16045
+ ...option2(options, "project") ? { projectIds: [option2(options, "project")] } : {}
16046
+ });
16047
+ } else {
16048
+ const workerId = option2(options, "worker");
16049
+ if (!workerId)
16050
+ throw new Error(`worker ${action} requires --worker`);
16051
+ if (action === "rotate-secret")
16052
+ body = await client2.rotateWorkerSecret(organizationId, workerId);
16053
+ else if (action === "move") {
16054
+ const targetWorkerIds = (option2(options, "target-workers") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
16055
+ if (!targetWorkerIds.length)
16056
+ throw new Error("worker move requires --target-workers <id,id,...>");
16057
+ body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/move`, { method: "POST", body: JSON.stringify({ targetWorkerIds }) });
16058
+ } else if (action === "approve-update") {
16059
+ const version = option2(options, "version");
16060
+ if (!version)
16061
+ throw new Error("worker approve-update requires --version");
16062
+ body = await client2.approveWorkerUpdate(organizationId, workerId, version);
16063
+ } else if (action === "drain" || action === "resume")
16064
+ body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/${action}`, { method: "POST" });
16065
+ else
16066
+ throw new Error(`unsupported worker command '${action}'`);
16067
+ }
16068
+ if (options.json || action !== "list")
16069
+ console.log(JSON.stringify(body, null, 2));
16070
+ else
16071
+ for (const candidate of Array.isArray(body.workers) ? body.workers : []) {
16072
+ const row = objectRecord(candidate);
16073
+ console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.type ?? "unknown"} ${row.state ?? "unknown"}`);
16074
+ }
16075
+ }
16076
+ async function registry(args) {
16077
+ const [action = "list", ...rest] = args;
16078
+ const options = parse2(rest);
16079
+ const organizationId = option2(options, "organization");
16080
+ const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
16081
+ const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
16082
+ if (!organizationId)
16083
+ throw new Error("registry command requires --organization");
16084
+ if (!apiUrl || !apiKey)
16085
+ throw new Error("registry command requires Sandblocks API URL and key");
16086
+ const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
16087
+ let body;
16088
+ if (action === "list")
16089
+ body = await client2.listRegistries(organizationId);
16090
+ else if (action === "create") {
16091
+ const name = option2(options, "name");
16092
+ const endpoint = option2(options, "endpoint");
16093
+ if (!name || !endpoint)
16094
+ throw new Error("registry create requires --name and --endpoint");
16095
+ body = await client2.createRegistry({ organizationId, name, endpoint });
16096
+ } else if (action === "assign") {
16097
+ const projectId = option2(options, "project");
16098
+ const registryId = option2(options, "registry");
16099
+ const purpose = option2(options, "purpose");
16100
+ if (!projectId || !registryId || !["build-cache", "artifact-publication", "deployment-pull"].includes(purpose ?? ""))
16101
+ throw new Error("registry assign requires --project, --registry, and a valid --purpose");
16102
+ body = await client2.assignProjectRegistry(projectId, {
16103
+ purpose,
16104
+ registryId,
16105
+ ...option2(options, "repository-prefix") ? { repositoryPrefix: option2(options, "repository-prefix") } : {}
16106
+ });
16107
+ } else
16108
+ throw new Error(`unsupported registry command '${action}'`);
16109
+ if (options.json || action !== "list")
16110
+ console.log(JSON.stringify(body, null, 2));
16111
+ else
16112
+ for (const candidate of Array.isArray(body.registries) ? body.registries : []) {
16113
+ const row = objectRecord(candidate);
16114
+ console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.endpoint ?? "unknown"}`);
16115
+ }
16116
+ }
15482
16117
  function printResources(kind, action, body) {
15483
16118
  if (action === "settings") {
15484
16119
  console.log(JSON.stringify(body.settings ?? {}, null, 2));
@@ -15505,10 +16140,7 @@ async function sandbox(args) {
15505
16140
  parse: parse2,
15506
16141
  option: option2,
15507
16142
  load,
15508
- source: async (root2) => {
15509
- const bundle2 = await createSourceTar(root2);
15510
- return { bundle: bundle2, files: sourceFileCount };
15511
- }
16143
+ source: createSourceTar
15512
16144
  });
15513
16145
  }
15514
16146
  if (subcommand === "deploy")
@@ -15533,7 +16165,8 @@ async function sandbox(args) {
15533
16165
  throw new Error("sandbox import requires --api-url or SANDBLOCKS_API_URL");
15534
16166
  if (!apiKey)
15535
16167
  throw new Error("sandbox import requires --api-key or SANDBLOCKS_API_KEY");
15536
- const bundle = await createSourceTar(root);
16168
+ const source = await createSourceTar(root);
16169
+ const bundle = source.bundle;
15537
16170
  const response = await fetch(`${apiUrl}/v1/projects/${encodeURIComponent(projectId)}/workspaces/import`, {
15538
16171
  method: "POST",
15539
16172
  headers: {
@@ -15550,11 +16183,11 @@ async function sandbox(args) {
15550
16183
  if (!response.ok)
15551
16184
  throw new Error(String(submitted.error ?? `sandbox import failed (${response.status})`));
15552
16185
  const operation = options.wait ? await waitForOperation(apiUrl, apiKey, String(submitted.operation.id)) : submitted.operation;
15553
- const output = { workspaceId, files: sourceFileCount, bytes: bundle.byteLength, operation };
16186
+ const output = { workspaceId, files: source.files, bytes: bundle.byteLength, operation };
15554
16187
  if (options.json)
15555
16188
  console.log(JSON.stringify(output, null, 2));
15556
16189
  else {
15557
- console.log(`source ${sourceFileCount} files (${bundle.byteLength} bytes)`);
16190
+ console.log(`source ${source.files} files (${bundle.byteLength} bytes)`);
15558
16191
  console.log(`workspace ${workspaceId}`);
15559
16192
  console.log(`operation ${operation.id} (${operation.state})`);
15560
16193
  if (operation.result?.container)
@@ -15647,7 +16280,6 @@ async function sandboxPromote(args) {
15647
16280
  else
15648
16281
  console.log(`promoted ${sandboxId} to production`);
15649
16282
  }
15650
- var sourceFileCount = 0;
15651
16283
  async function createPrebuiltTar(root) {
15652
16284
  const names = [];
15653
16285
  const visit2 = async (directory, prefix = "") => {
@@ -15693,7 +16325,7 @@ async function createPrebuiltTar(root) {
15693
16325
  }
15694
16326
  return output;
15695
16327
  }
15696
- async function createSourceTar(root) {
16328
+ async function createSourceTar(root, previous) {
15697
16329
  const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
15698
16330
  cwd: root,
15699
16331
  stdout: "pipe",
@@ -15714,6 +16346,7 @@ async function createSourceTar(root) {
15714
16346
  if (names.length > 20000)
15715
16347
  throw new Error("local source contains more than 20000 files");
15716
16348
  const chunks = [];
16349
+ const manifest = {};
15717
16350
  let total = 1024;
15718
16351
  for (const name of names) {
15719
16352
  if (name.includes("\x00") || name.includes("\\") || name.startsWith("/") || name.split("/").includes("..")) {
@@ -15733,6 +16366,11 @@ async function createSourceTar(root) {
15733
16366
  if (!info.isFile())
15734
16367
  continue;
15735
16368
  const contents = new Uint8Array(await readFile8(file));
16369
+ const mode = info.mode & 511;
16370
+ const sha256 = createHash3("sha256").update(contents).digest("hex");
16371
+ manifest[name] = { sha256, mode, bytes: contents.byteLength };
16372
+ if (previous?.[name]?.sha256 === sha256 && previous[name]?.mode === mode)
16373
+ continue;
15736
16374
  const header = tarHeader(name, contents.byteLength, info.mode, Math.floor(info.mtimeMs / 1000));
15737
16375
  const padding = (512 - contents.byteLength % 512) % 512;
15738
16376
  total += 512 + contents.byteLength + padding;
@@ -15740,9 +16378,15 @@ async function createSourceTar(root) {
15740
16378
  throw new Error("local source bundle exceeds 256 MiB");
15741
16379
  chunks.push(header, contents, new Uint8Array(padding));
15742
16380
  }
15743
- sourceFileCount = chunks.length / 3;
15744
- if (!sourceFileCount)
16381
+ if (!Object.keys(manifest).length)
15745
16382
  throw new Error("local directory has no regular source files");
16383
+ if (previous) {
16384
+ const contents = new TextEncoder().encode(JSON.stringify({ version: 1, files: manifest }));
16385
+ const name = ".sandblocks-sync-manifest.json";
16386
+ const padding = (512 - contents.byteLength % 512) % 512;
16387
+ total += 512 + contents.byteLength + padding;
16388
+ chunks.push(tarHeader(name, contents.byteLength, 384, Math.floor(Date.now() / 1000)), contents, new Uint8Array(padding));
16389
+ }
15746
16390
  chunks.push(new Uint8Array(1024));
15747
16391
  const output = new Uint8Array(total);
15748
16392
  let offset = 0;
@@ -15750,7 +16394,7 @@ async function createSourceTar(root) {
15750
16394
  output.set(chunk, offset);
15751
16395
  offset += chunk.byteLength;
15752
16396
  }
15753
- return output;
16397
+ return { bundle: output, files: Object.keys(manifest).length, manifest };
15754
16398
  }
15755
16399
  async function gitIgnoredFiles(root, names) {
15756
16400
  if (!names.length)
@@ -15936,7 +16580,8 @@ function parse2(args) {
15936
16580
  "allow-host",
15937
16581
  "rootless",
15938
16582
  "allow-unpinned-images",
15939
- "read-only-root"
16583
+ "read-only-root",
16584
+ "resume"
15940
16585
  ].includes(rawKey)) {
15941
16586
  output[rawKey] = inline === undefined ? true : inline !== "false";
15942
16587
  continue;
@@ -16003,4 +16648,4 @@ export {
16003
16648
  parse2 as parse
16004
16649
  };
16005
16650
 
16006
- //# debugId=E7F4E37BF2F49E8E64756E2164756E21
16651
+ //# debugId=ACB6B270856F84F064756E2164756E21