@sandblocks/cli 0.5.0-b.1 → 0.5.0-b.11

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
@@ -18,7 +18,7 @@ var __export = (target, all) => {
18
18
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = import.meta.require;
20
20
 
21
- // ../manifest/src/project-graph.ts
21
+ // packages/libs/manifest/src/project-graph.ts
22
22
  function compileProjectGraph(manifest) {
23
23
  return {
24
24
  version: 2,
@@ -77,9 +77,9 @@ function compileProjectGraph(manifest) {
77
77
  pipeline: environment.pipeline,
78
78
  services: environment.services,
79
79
  checks: environment.checks,
80
- baseDomain: environment.routing.baseDomain,
81
- domains: environment.routing.domains,
82
- stableDomains: environment.routing.stableDomains,
80
+ baseDomain: "sandblocks.dev",
81
+ domains: {},
82
+ stableDomains: {},
83
83
  ...environment.targetHost ? { targetHost: environment.targetHost } : {},
84
84
  ...typeof environment.routing.sso === "boolean" ? { ssoEnabled: environment.routing.sso } : {}
85
85
  }))
@@ -89,7 +89,7 @@ function normalizeCommand(command) {
89
89
  return typeof command === "string" ? ["sh", "-lc", command] : [...command];
90
90
  }
91
91
 
92
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
92
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
93
93
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
94
94
  const t = typeof data;
95
95
  switch (t) {
@@ -220,7 +220,7 @@ var init_util = __esm(() => {
220
220
  ]);
221
221
  });
222
222
 
223
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
223
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
224
224
  var ZodIssueCode, quotelessJson = (obj) => {
225
225
  const json = JSON.stringify(obj, null, 2);
226
226
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -341,7 +341,7 @@ var init_ZodError = __esm(() => {
341
341
  };
342
342
  });
343
343
 
344
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
344
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
345
345
  var errorMap = (issue, _ctx) => {
346
346
  let message;
347
347
  switch (issue.code) {
@@ -448,7 +448,7 @@ var init_en = __esm(() => {
448
448
  en_default = errorMap;
449
449
  });
450
450
 
451
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
451
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
452
452
  function setErrorMap(map) {
453
453
  overrideErrorMap = map;
454
454
  }
@@ -461,7 +461,7 @@ var init_errors = __esm(() => {
461
461
  overrideErrorMap = en_default;
462
462
  });
463
463
 
464
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
464
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
465
465
  function addIssueToContext(ctx, issueData) {
466
466
  const overrideMap = getErrorMap();
467
467
  const issue = makeIssue({
@@ -566,10 +566,10 @@ var init_parseUtil = __esm(() => {
566
566
  });
567
567
  });
568
568
 
569
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
569
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
570
570
  var init_typeAliases = () => {};
571
571
 
572
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
572
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
573
573
  var errorUtil;
574
574
  var init_errorUtil = __esm(() => {
575
575
  (function(errorUtil2) {
@@ -578,7 +578,7 @@ var init_errorUtil = __esm(() => {
578
578
  })(errorUtil || (errorUtil = {}));
579
579
  });
580
580
 
581
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
581
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
582
582
  class ParseInputLazyPath {
583
583
  constructor(parent, value, path, key) {
584
584
  this._cachedPath = [];
@@ -3929,7 +3929,7 @@ var init_types = __esm(() => {
3929
3929
  NEVER = INVALID;
3930
3930
  });
3931
3931
 
3932
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
3932
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
3933
3933
  var exports_external = {};
3934
3934
  __export(exports_external, {
3935
3935
  void: () => voidType,
@@ -4049,13 +4049,13 @@ var init_external = __esm(() => {
4049
4049
  init_ZodError();
4050
4050
  });
4051
4051
 
4052
- // ../../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4052
+ // node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4053
4053
  var init_zod = __esm(() => {
4054
4054
  init_external();
4055
4055
  init_external();
4056
4056
  });
4057
4057
 
4058
- // ../manifest/src/schema.ts
4058
+ // packages/libs/manifest/src/schema.ts
4059
4059
  function keyedCollection(schema) {
4060
4060
  return exports_external.record(id, schema).transform((values) => Object.entries(values).map(([entryId, value]) => ({ id: entryId, ...value })));
4061
4061
  }
@@ -4079,23 +4079,6 @@ function validateReferences(manifest, ctx) {
4079
4079
  validateReferencesTo(ctx, environment.pipeline, taskIds, ["environments", index, "pipeline"], "task");
4080
4080
  validateReferencesTo(ctx, environment.services, serviceIds, ["environments", index, "services"], "service");
4081
4081
  validateReferencesTo(ctx, environment.checks, checkIds, ["environments", index, "checks"], "check");
4082
- validateReferencesTo(ctx, Object.keys(environment.routing.domains), new Set(environment.services), ["environments", index, "routing", "domains"], "environment service");
4083
- validateReferencesTo(ctx, Object.keys(environment.routing.stableDomains), new Set(environment.services), ["environments", index, "routing", "stableDomains"], "environment service");
4084
- for (const [service, configured] of Object.entries({
4085
- ...environment.routing.domains,
4086
- ...environment.routing.stableDomains
4087
- })) {
4088
- for (const template of Array.isArray(configured) ? configured : [configured]) {
4089
- const expanded = template.replaceAll(/\{(?:sandbox|revision|service)\}/g, "preview");
4090
- if (expanded.includes("{") || !/^[a-z0-9.-]+$/.test(expanded.toLowerCase())) {
4091
- ctx.addIssue({
4092
- code: exports_external.ZodIssueCode.custom,
4093
- path: ["environments", index, "routing", "domains", service],
4094
- message: "domain template may use only {sandbox}, {revision}, and {service}"
4095
- });
4096
- }
4097
- }
4098
- }
4099
4082
  }
4100
4083
  }
4101
4084
  function validateAcyclicServices(manifest, ctx) {
@@ -4269,12 +4252,7 @@ var init_schema = __esm(() => {
4269
4252
  services: exports_external.array(id).min(1),
4270
4253
  checks: exports_external.array(id).default([]),
4271
4254
  targetHost: exports_external.string().min(1).max(255).optional(),
4272
- routing: exports_external.object({
4273
- baseDomain: exports_external.string().min(1).max(253).default("sandblocks.dev"),
4274
- domains: exports_external.record(id, exports_external.string().min(1).max(253)).default({}),
4275
- 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({}),
4276
- sso: exports_external.boolean().optional()
4277
- }).strict().default({})
4255
+ routing: exports_external.object({ sso: exports_external.boolean().optional() }).strict().default({})
4278
4256
  }).strict();
4279
4257
  EnvironmentsV2Schema = keyedCollection(EnvironmentValueSchema);
4280
4258
  HooksV2Schema = exports_external.object({
@@ -4302,7 +4280,7 @@ var init_schema = __esm(() => {
4302
4280
  SandblocksManifestV2Schema = CompleteSandblocksManifestV2Schema.superRefine(validateReferences);
4303
4281
  });
4304
4282
 
4305
- // ../manifest/src/resolve.ts
4283
+ // packages/libs/manifest/src/resolve.ts
4306
4284
  function resolveProjectGraph(manifest) {
4307
4285
  return compileProjectGraph(manifest);
4308
4286
  }
@@ -4403,9 +4381,16 @@ function executionService(service) {
4403
4381
  }
4404
4382
  var init_resolve = () => {};
4405
4383
 
4406
- // ../manifest/src/images.ts
4384
+ // packages/libs/manifest/src/images.ts
4407
4385
  import { readFile, realpath, stat } from "fs/promises";
4408
4386
  import path from "path";
4387
+ function resolvePrebuiltImage(family, registry = process.env.SANDBLOCKS_PREBUILT_IMAGE_REGISTRY) {
4388
+ const image = SANDBLOCKS_PREBUILT_IMAGES[family];
4389
+ const configured = registry?.trim().replace(/\/$/, "");
4390
+ if (!configured)
4391
+ return image;
4392
+ return `${configured}/${image.slice(image.lastIndexOf("/") + 1)}`;
4393
+ }
4409
4394
  async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".") {
4410
4395
  const root = await realpath(repositoryRoot);
4411
4396
  const workspace = await containedDirectory(root, workspaceRoot);
@@ -4436,7 +4421,7 @@ async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".")
4436
4421
  return selection("node", "default Node.js environment");
4437
4422
  }
4438
4423
  function selection(family, evidence) {
4439
- return { family, image: SANDBLOCKS_PREBUILT_IMAGES[family], evidence };
4424
+ return { family, image: resolvePrebuiltImage(family), evidence };
4440
4425
  }
4441
4426
  async function containedDirectory(repositoryRoot, relative) {
4442
4427
  const candidate = path.resolve(repositoryRoot, relative);
@@ -4502,14 +4487,14 @@ function relativeEvidence(repositoryRoot, directory, file) {
4502
4487
  var SANDBLOCKS_PREBUILT_IMAGES, LOCKFILES;
4503
4488
  var init_images = __esm(() => {
4504
4489
  SANDBLOCKS_PREBUILT_IMAGES = {
4505
- bun: "ghcr.io/honeycluster/sandblocks-executor-bun:1.0.0",
4506
- pnpm: "ghcr.io/honeycluster/sandblocks-executor-pnpm:1.0.0",
4507
- npm: "ghcr.io/honeycluster/sandblocks-executor-npm:1.0.0",
4508
- yarn: "ghcr.io/honeycluster/sandblocks-executor-yarn:1.0.0",
4509
- corepack: "ghcr.io/honeycluster/sandblocks-executor-corepack:1.0.0",
4510
- node: "ghcr.io/honeycluster/sandblocks-executor-node:1.0.0",
4511
- "node-native": "ghcr.io/honeycluster/sandblocks-executor-node-native:1.0.0",
4512
- playwright: "ghcr.io/honeycluster/sandblocks-executor-playwright:1.0.0"
4490
+ bun: "100.114.99.85:5000/sandblocks/executor-bun:1.0.0",
4491
+ pnpm: "100.114.99.85:5000/sandblocks/executor-pnpm:1.2.0",
4492
+ npm: "100.114.99.85:5000/sandblocks/executor-npm:1.0.0",
4493
+ yarn: "100.114.99.85:5000/sandblocks/executor-yarn:1.0.0",
4494
+ corepack: "100.114.99.85:5000/sandblocks/executor-corepack:1.0.0",
4495
+ node: "100.114.99.85:5000/sandblocks/executor-node:1.0.0",
4496
+ "node-native": "100.114.99.85:5000/sandblocks/executor-node-native:1.0.0",
4497
+ playwright: "100.114.99.85:5000/sandblocks/executor-playwright:1.0.0"
4513
4498
  };
4514
4499
  LOCKFILES = [
4515
4500
  { file: "bun.lock", family: "bun" },
@@ -4521,7 +4506,7 @@ var init_images = __esm(() => {
4521
4506
  ];
4522
4507
  });
4523
4508
 
4524
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
4509
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
4525
4510
  var require_identity = __commonJS((exports) => {
4526
4511
  var ALIAS = Symbol.for("yaml.alias");
4527
4512
  var DOC = Symbol.for("yaml.document");
@@ -4575,7 +4560,7 @@ var require_identity = __commonJS((exports) => {
4575
4560
  exports.isSeq = isSeq;
4576
4561
  });
4577
4562
 
4578
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/visit.js
4563
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/visit.js
4579
4564
  var require_visit = __commonJS((exports) => {
4580
4565
  var identity = require_identity();
4581
4566
  var BREAK = Symbol("break visit");
@@ -4730,7 +4715,7 @@ var require_visit = __commonJS((exports) => {
4730
4715
  exports.visitAsync = visitAsync;
4731
4716
  });
4732
4717
 
4733
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js
4718
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js
4734
4719
  var require_directives = __commonJS((exports) => {
4735
4720
  var identity = require_identity();
4736
4721
  var visit = require_visit();
@@ -4882,7 +4867,7 @@ var require_directives = __commonJS((exports) => {
4882
4867
  exports.Directives = Directives;
4883
4868
  });
4884
4869
 
4885
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js
4870
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js
4886
4871
  var require_anchors = __commonJS((exports) => {
4887
4872
  var identity = require_identity();
4888
4873
  var visit = require_visit();
@@ -4944,7 +4929,7 @@ var require_anchors = __commonJS((exports) => {
4944
4929
  exports.findNewAnchor = findNewAnchor;
4945
4930
  });
4946
4931
 
4947
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/applyReviver.js
4932
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/applyReviver.js
4948
4933
  var require_applyReviver = __commonJS((exports) => {
4949
4934
  function applyReviver(reviver, obj, key, val) {
4950
4935
  if (val && typeof val === "object") {
@@ -4991,7 +4976,7 @@ var require_applyReviver = __commonJS((exports) => {
4991
4976
  exports.applyReviver = applyReviver;
4992
4977
  });
4993
4978
 
4994
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/toJS.js
4979
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/toJS.js
4995
4980
  var require_toJS = __commonJS((exports) => {
4996
4981
  var identity = require_identity();
4997
4982
  function toJS(value, arg, ctx) {
@@ -5018,7 +5003,7 @@ var require_toJS = __commonJS((exports) => {
5018
5003
  exports.toJS = toJS;
5019
5004
  });
5020
5005
 
5021
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Node.js
5006
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Node.js
5022
5007
  var require_Node = __commonJS((exports) => {
5023
5008
  var applyReviver = require_applyReviver();
5024
5009
  var identity = require_identity();
@@ -5055,7 +5040,7 @@ var require_Node = __commonJS((exports) => {
5055
5040
  exports.NodeBase = NodeBase;
5056
5041
  });
5057
5042
 
5058
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js
5043
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js
5059
5044
  var require_Alias = __commonJS((exports) => {
5060
5045
  var anchors = require_anchors();
5061
5046
  var visit = require_visit();
@@ -5165,7 +5150,7 @@ var require_Alias = __commonJS((exports) => {
5165
5150
  exports.Alias = Alias;
5166
5151
  });
5167
5152
 
5168
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Scalar.js
5153
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Scalar.js
5169
5154
  var require_Scalar = __commonJS((exports) => {
5170
5155
  var identity = require_identity();
5171
5156
  var Node = require_Node();
@@ -5193,7 +5178,7 @@ var require_Scalar = __commonJS((exports) => {
5193
5178
  exports.isScalarValue = isScalarValue;
5194
5179
  });
5195
5180
 
5196
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/createNode.js
5181
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/createNode.js
5197
5182
  var require_createNode = __commonJS((exports) => {
5198
5183
  var Alias = require_Alias();
5199
5184
  var identity = require_identity();
@@ -5265,7 +5250,7 @@ var require_createNode = __commonJS((exports) => {
5265
5250
  exports.createNode = createNode;
5266
5251
  });
5267
5252
 
5268
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Collection.js
5253
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Collection.js
5269
5254
  var require_Collection = __commonJS((exports) => {
5270
5255
  var createNode = require_createNode();
5271
5256
  var identity = require_identity();
@@ -5380,7 +5365,7 @@ var require_Collection = __commonJS((exports) => {
5380
5365
  exports.isEmptyPath = isEmptyPath;
5381
5366
  });
5382
5367
 
5383
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyComment.js
5368
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyComment.js
5384
5369
  var require_stringifyComment = __commonJS((exports) => {
5385
5370
  var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#");
5386
5371
  function indentComment(comment, indent) {
@@ -5397,7 +5382,7 @@ var require_stringifyComment = __commonJS((exports) => {
5397
5382
  exports.stringifyComment = stringifyComment;
5398
5383
  });
5399
5384
 
5400
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/foldFlowLines.js
5385
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/foldFlowLines.js
5401
5386
  var require_foldFlowLines = __commonJS((exports) => {
5402
5387
  var FOLD_FLOW = "flow";
5403
5388
  var FOLD_BLOCK = "block";
@@ -5534,7 +5519,7 @@ ${indent}${text.slice(fold + 1, end2)}`;
5534
5519
  exports.foldFlowLines = foldFlowLines;
5535
5520
  });
5536
5521
 
5537
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyString.js
5522
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyString.js
5538
5523
  var require_stringifyString = __commonJS((exports) => {
5539
5524
  var Scalar = require_Scalar();
5540
5525
  var foldFlowLines = require_foldFlowLines();
@@ -5832,7 +5817,7 @@ ${indent}`);
5832
5817
  exports.stringifyString = stringifyString;
5833
5818
  });
5834
5819
 
5835
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringify.js
5820
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringify.js
5836
5821
  var require_stringify = __commonJS((exports) => {
5837
5822
  var anchors = require_anchors();
5838
5823
  var identity = require_identity();
@@ -5953,7 +5938,7 @@ ${ctx.indent}${str}`;
5953
5938
  exports.stringify = stringify;
5954
5939
  });
5955
5940
 
5956
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyPair.js
5941
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyPair.js
5957
5942
  var require_stringifyPair = __commonJS((exports) => {
5958
5943
  var identity = require_identity();
5959
5944
  var Scalar = require_Scalar();
@@ -6089,7 +6074,7 @@ ${ctx.indent}`;
6089
6074
  exports.stringifyPair = stringifyPair;
6090
6075
  });
6091
6076
 
6092
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/log.js
6077
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/log.js
6093
6078
  var require_log = __commonJS((exports) => {
6094
6079
  var node_process = __require("process");
6095
6080
  function debug(logLevel, ...messages) {
@@ -6108,7 +6093,7 @@ var require_log = __commonJS((exports) => {
6108
6093
  exports.warn = warn;
6109
6094
  });
6110
6095
 
6111
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/merge.js
6096
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/merge.js
6112
6097
  var require_merge = __commonJS((exports) => {
6113
6098
  var identity = require_identity();
6114
6099
  var Scalar = require_Scalar();
@@ -6165,7 +6150,7 @@ var require_merge = __commonJS((exports) => {
6165
6150
  exports.merge = merge;
6166
6151
  });
6167
6152
 
6168
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
6153
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
6169
6154
  var require_addPairToJSMap = __commonJS((exports) => {
6170
6155
  var log = require_log();
6171
6156
  var merge = require_merge();
@@ -6226,7 +6211,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
6226
6211
  exports.addPairToJSMap = addPairToJSMap;
6227
6212
  });
6228
6213
 
6229
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Pair.js
6214
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Pair.js
6230
6215
  var require_Pair = __commonJS((exports) => {
6231
6216
  var createNode = require_createNode();
6232
6217
  var stringifyPair = require_stringifyPair();
@@ -6264,7 +6249,7 @@ var require_Pair = __commonJS((exports) => {
6264
6249
  exports.createPair = createPair;
6265
6250
  });
6266
6251
 
6267
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyCollection.js
6252
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyCollection.js
6268
6253
  var require_stringifyCollection = __commonJS((exports) => {
6269
6254
  var identity = require_identity();
6270
6255
  var stringify = require_stringify();
@@ -6416,7 +6401,7 @@ ${indent}${end}`;
6416
6401
  exports.stringifyCollection = stringifyCollection;
6417
6402
  });
6418
6403
 
6419
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLMap.js
6404
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLMap.js
6420
6405
  var require_YAMLMap = __commonJS((exports) => {
6421
6406
  var stringifyCollection = require_stringifyCollection();
6422
6407
  var addPairToJSMap = require_addPairToJSMap();
@@ -6543,7 +6528,7 @@ var require_YAMLMap = __commonJS((exports) => {
6543
6528
  exports.findPair = findPair;
6544
6529
  });
6545
6530
 
6546
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/map.js
6531
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/map.js
6547
6532
  var require_map = __commonJS((exports) => {
6548
6533
  var identity = require_identity();
6549
6534
  var YAMLMap = require_YAMLMap();
@@ -6562,7 +6547,7 @@ var require_map = __commonJS((exports) => {
6562
6547
  exports.map = map;
6563
6548
  });
6564
6549
 
6565
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLSeq.js
6550
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLSeq.js
6566
6551
  var require_YAMLSeq = __commonJS((exports) => {
6567
6552
  var createNode = require_createNode();
6568
6553
  var stringifyCollection = require_stringifyCollection();
@@ -6655,7 +6640,7 @@ var require_YAMLSeq = __commonJS((exports) => {
6655
6640
  exports.YAMLSeq = YAMLSeq;
6656
6641
  });
6657
6642
 
6658
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/seq.js
6643
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/seq.js
6659
6644
  var require_seq = __commonJS((exports) => {
6660
6645
  var identity = require_identity();
6661
6646
  var YAMLSeq = require_YAMLSeq();
@@ -6674,7 +6659,7 @@ var require_seq = __commonJS((exports) => {
6674
6659
  exports.seq = seq;
6675
6660
  });
6676
6661
 
6677
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/string.js
6662
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/string.js
6678
6663
  var require_string = __commonJS((exports) => {
6679
6664
  var stringifyString = require_stringifyString();
6680
6665
  var string = {
@@ -6690,7 +6675,7 @@ var require_string = __commonJS((exports) => {
6690
6675
  exports.string = string;
6691
6676
  });
6692
6677
 
6693
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/null.js
6678
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/null.js
6694
6679
  var require_null = __commonJS((exports) => {
6695
6680
  var Scalar = require_Scalar();
6696
6681
  var nullTag = {
@@ -6705,7 +6690,7 @@ var require_null = __commonJS((exports) => {
6705
6690
  exports.nullTag = nullTag;
6706
6691
  });
6707
6692
 
6708
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/bool.js
6693
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/bool.js
6709
6694
  var require_bool = __commonJS((exports) => {
6710
6695
  var Scalar = require_Scalar();
6711
6696
  var boolTag = {
@@ -6726,7 +6711,7 @@ var require_bool = __commonJS((exports) => {
6726
6711
  exports.boolTag = boolTag;
6727
6712
  });
6728
6713
 
6729
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyNumber.js
6714
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyNumber.js
6730
6715
  var require_stringifyNumber = __commonJS((exports) => {
6731
6716
  function stringifyNumber({ format, minFractionDigits, tag, value }) {
6732
6717
  if (typeof value === "bigint")
@@ -6750,7 +6735,7 @@ var require_stringifyNumber = __commonJS((exports) => {
6750
6735
  exports.stringifyNumber = stringifyNumber;
6751
6736
  });
6752
6737
 
6753
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/float.js
6738
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/float.js
6754
6739
  var require_float = __commonJS((exports) => {
6755
6740
  var Scalar = require_Scalar();
6756
6741
  var stringifyNumber = require_stringifyNumber();
@@ -6793,7 +6778,7 @@ var require_float = __commonJS((exports) => {
6793
6778
  exports.floatNaN = floatNaN;
6794
6779
  });
6795
6780
 
6796
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/int.js
6781
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/int.js
6797
6782
  var require_int = __commonJS((exports) => {
6798
6783
  var stringifyNumber = require_stringifyNumber();
6799
6784
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
@@ -6835,7 +6820,7 @@ var require_int = __commonJS((exports) => {
6835
6820
  exports.intOct = intOct;
6836
6821
  });
6837
6822
 
6838
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/schema.js
6823
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/schema.js
6839
6824
  var require_schema = __commonJS((exports) => {
6840
6825
  var map = require_map();
6841
6826
  var _null = require_null();
@@ -6860,7 +6845,7 @@ var require_schema = __commonJS((exports) => {
6860
6845
  exports.schema = schema;
6861
6846
  });
6862
6847
 
6863
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/json/schema.js
6848
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/json/schema.js
6864
6849
  var require_schema2 = __commonJS((exports) => {
6865
6850
  var Scalar = require_Scalar();
6866
6851
  var map = require_map();
@@ -6924,7 +6909,7 @@ var require_schema2 = __commonJS((exports) => {
6924
6909
  exports.schema = schema;
6925
6910
  });
6926
6911
 
6927
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/binary.js
6912
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/binary.js
6928
6913
  var require_binary = __commonJS((exports) => {
6929
6914
  var node_buffer = __require("buffer");
6930
6915
  var Scalar = require_Scalar();
@@ -6979,7 +6964,7 @@ var require_binary = __commonJS((exports) => {
6979
6964
  exports.binary = binary;
6980
6965
  });
6981
6966
 
6982
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/pairs.js
6967
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/pairs.js
6983
6968
  var require_pairs = __commonJS((exports) => {
6984
6969
  var identity = require_identity();
6985
6970
  var Pair = require_Pair();
@@ -7054,7 +7039,7 @@ ${cn.comment}` : item.comment;
7054
7039
  exports.resolvePairs = resolvePairs;
7055
7040
  });
7056
7041
 
7057
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/omap.js
7042
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/omap.js
7058
7043
  var require_omap = __commonJS((exports) => {
7059
7044
  var identity = require_identity();
7060
7045
  var toJS = require_toJS();
@@ -7126,7 +7111,7 @@ var require_omap = __commonJS((exports) => {
7126
7111
  exports.omap = omap;
7127
7112
  });
7128
7113
 
7129
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/bool.js
7114
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/bool.js
7130
7115
  var require_bool2 = __commonJS((exports) => {
7131
7116
  var Scalar = require_Scalar();
7132
7117
  function boolStringify({ value, source }, ctx) {
@@ -7155,7 +7140,7 @@ var require_bool2 = __commonJS((exports) => {
7155
7140
  exports.trueTag = trueTag;
7156
7141
  });
7157
7142
 
7158
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/float.js
7143
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/float.js
7159
7144
  var require_float2 = __commonJS((exports) => {
7160
7145
  var Scalar = require_Scalar();
7161
7146
  var stringifyNumber = require_stringifyNumber();
@@ -7201,7 +7186,7 @@ var require_float2 = __commonJS((exports) => {
7201
7186
  exports.floatNaN = floatNaN;
7202
7187
  });
7203
7188
 
7204
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/int.js
7189
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/int.js
7205
7190
  var require_int2 = __commonJS((exports) => {
7206
7191
  var stringifyNumber = require_stringifyNumber();
7207
7192
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
@@ -7277,7 +7262,7 @@ var require_int2 = __commonJS((exports) => {
7277
7262
  exports.intOct = intOct;
7278
7263
  });
7279
7264
 
7280
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/set.js
7265
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/set.js
7281
7266
  var require_set = __commonJS((exports) => {
7282
7267
  var identity = require_identity();
7283
7268
  var Pair = require_Pair();
@@ -7360,7 +7345,7 @@ var require_set = __commonJS((exports) => {
7360
7345
  exports.set = set;
7361
7346
  });
7362
7347
 
7363
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js
7348
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js
7364
7349
  var require_timestamp = __commonJS((exports) => {
7365
7350
  var stringifyNumber = require_stringifyNumber();
7366
7351
  function parseSexagesimal(str, asBigInt) {
@@ -7442,7 +7427,7 @@ var require_timestamp = __commonJS((exports) => {
7442
7427
  exports.timestamp = timestamp;
7443
7428
  });
7444
7429
 
7445
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
7430
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
7446
7431
  var require_schema3 = __commonJS((exports) => {
7447
7432
  var map = require_map();
7448
7433
  var _null = require_null();
@@ -7483,7 +7468,7 @@ var require_schema3 = __commonJS((exports) => {
7483
7468
  exports.schema = schema;
7484
7469
  });
7485
7470
 
7486
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/tags.js
7471
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/tags.js
7487
7472
  var require_tags = __commonJS((exports) => {
7488
7473
  var map = require_map();
7489
7474
  var _null = require_null();
@@ -7574,7 +7559,7 @@ var require_tags = __commonJS((exports) => {
7574
7559
  exports.getTags = getTags;
7575
7560
  });
7576
7561
 
7577
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/Schema.js
7562
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/Schema.js
7578
7563
  var require_Schema = __commonJS((exports) => {
7579
7564
  var identity = require_identity();
7580
7565
  var map = require_map();
@@ -7604,7 +7589,7 @@ var require_Schema = __commonJS((exports) => {
7604
7589
  exports.Schema = Schema;
7605
7590
  });
7606
7591
 
7607
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyDocument.js
7592
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyDocument.js
7608
7593
  var require_stringifyDocument = __commonJS((exports) => {
7609
7594
  var identity = require_identity();
7610
7595
  var stringify = require_stringify();
@@ -7684,7 +7669,7 @@ var require_stringifyDocument = __commonJS((exports) => {
7684
7669
  exports.stringifyDocument = stringifyDocument;
7685
7670
  });
7686
7671
 
7687
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/Document.js
7672
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/Document.js
7688
7673
  var require_Document = __commonJS((exports) => {
7689
7674
  var Alias = require_Alias();
7690
7675
  var Collection = require_Collection();
@@ -7919,7 +7904,7 @@ var require_Document = __commonJS((exports) => {
7919
7904
  exports.Document = Document;
7920
7905
  });
7921
7906
 
7922
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/errors.js
7907
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/errors.js
7923
7908
  var require_errors = __commonJS((exports) => {
7924
7909
  class YAMLError extends Error {
7925
7910
  constructor(name, pos, code, message) {
@@ -7984,7 +7969,7 @@ ${pointer}
7984
7969
  exports.prettifyError = prettifyError;
7985
7970
  });
7986
7971
 
7987
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-props.js
7972
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-props.js
7988
7973
  var require_resolve_props = __commonJS((exports) => {
7989
7974
  function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) {
7990
7975
  let spaceBefore = false;
@@ -8114,7 +8099,7 @@ var require_resolve_props = __commonJS((exports) => {
8114
8099
  exports.resolveProps = resolveProps;
8115
8100
  });
8116
8101
 
8117
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-contains-newline.js
8102
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-contains-newline.js
8118
8103
  var require_util_contains_newline = __commonJS((exports) => {
8119
8104
  function containsNewline(key) {
8120
8105
  if (!key)
@@ -8154,7 +8139,7 @@ var require_util_contains_newline = __commonJS((exports) => {
8154
8139
  exports.containsNewline = containsNewline;
8155
8140
  });
8156
8141
 
8157
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-flow-indent-check.js
8142
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-flow-indent-check.js
8158
8143
  var require_util_flow_indent_check = __commonJS((exports) => {
8159
8144
  var utilContainsNewline = require_util_contains_newline();
8160
8145
  function flowIndentCheck(indent, fc, onError) {
@@ -8169,7 +8154,7 @@ var require_util_flow_indent_check = __commonJS((exports) => {
8169
8154
  exports.flowIndentCheck = flowIndentCheck;
8170
8155
  });
8171
8156
 
8172
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-map-includes.js
8157
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-map-includes.js
8173
8158
  var require_util_map_includes = __commonJS((exports) => {
8174
8159
  var identity = require_identity();
8175
8160
  function mapIncludes(ctx, items, search) {
@@ -8182,7 +8167,7 @@ var require_util_map_includes = __commonJS((exports) => {
8182
8167
  exports.mapIncludes = mapIncludes;
8183
8168
  });
8184
8169
 
8185
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-map.js
8170
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-map.js
8186
8171
  var require_resolve_block_map = __commonJS((exports) => {
8187
8172
  var Pair = require_Pair();
8188
8173
  var YAMLMap = require_YAMLMap();
@@ -8289,7 +8274,7 @@ var require_resolve_block_map = __commonJS((exports) => {
8289
8274
  exports.resolveBlockMap = resolveBlockMap;
8290
8275
  });
8291
8276
 
8292
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-seq.js
8277
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-seq.js
8293
8278
  var require_resolve_block_seq = __commonJS((exports) => {
8294
8279
  var YAMLSeq = require_YAMLSeq();
8295
8280
  var resolveProps = require_resolve_props();
@@ -8337,7 +8322,7 @@ var require_resolve_block_seq = __commonJS((exports) => {
8337
8322
  exports.resolveBlockSeq = resolveBlockSeq;
8338
8323
  });
8339
8324
 
8340
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-end.js
8325
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-end.js
8341
8326
  var require_resolve_end = __commonJS((exports) => {
8342
8327
  function resolveEnd(end, offset, reqSpace, onError) {
8343
8328
  let comment = "";
@@ -8377,7 +8362,7 @@ var require_resolve_end = __commonJS((exports) => {
8377
8362
  exports.resolveEnd = resolveEnd;
8378
8363
  });
8379
8364
 
8380
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-collection.js
8365
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-collection.js
8381
8366
  var require_resolve_flow_collection = __commonJS((exports) => {
8382
8367
  var identity = require_identity();
8383
8368
  var Pair = require_Pair();
@@ -8568,7 +8553,7 @@ var require_resolve_flow_collection = __commonJS((exports) => {
8568
8553
  exports.resolveFlowCollection = resolveFlowCollection;
8569
8554
  });
8570
8555
 
8571
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-collection.js
8556
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-collection.js
8572
8557
  var require_compose_collection = __commonJS((exports) => {
8573
8558
  var identity = require_identity();
8574
8559
  var Scalar = require_Scalar();
@@ -8630,7 +8615,7 @@ var require_compose_collection = __commonJS((exports) => {
8630
8615
  exports.composeCollection = composeCollection;
8631
8616
  });
8632
8617
 
8633
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-scalar.js
8618
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-scalar.js
8634
8619
  var require_resolve_block_scalar = __commonJS((exports) => {
8635
8620
  var Scalar = require_Scalar();
8636
8621
  function resolveBlockScalar(ctx, scalar, onError) {
@@ -8823,7 +8808,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
8823
8808
  exports.resolveBlockScalar = resolveBlockScalar;
8824
8809
  });
8825
8810
 
8826
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-scalar.js
8811
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-scalar.js
8827
8812
  var require_resolve_flow_scalar = __commonJS((exports) => {
8828
8813
  var Scalar = require_Scalar();
8829
8814
  var resolveEnd = require_resolve_end();
@@ -9040,7 +9025,7 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
9040
9025
  exports.resolveFlowScalar = resolveFlowScalar;
9041
9026
  });
9042
9027
 
9043
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-scalar.js
9028
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-scalar.js
9044
9029
  var require_compose_scalar = __commonJS((exports) => {
9045
9030
  var identity = require_identity();
9046
9031
  var Scalar = require_Scalar();
@@ -9118,7 +9103,7 @@ var require_compose_scalar = __commonJS((exports) => {
9118
9103
  exports.composeScalar = composeScalar;
9119
9104
  });
9120
9105
 
9121
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-empty-scalar-position.js
9106
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-empty-scalar-position.js
9122
9107
  var require_util_empty_scalar_position = __commonJS((exports) => {
9123
9108
  function emptyScalarPosition(offset, before, pos) {
9124
9109
  if (before) {
@@ -9145,7 +9130,7 @@ var require_util_empty_scalar_position = __commonJS((exports) => {
9145
9130
  exports.emptyScalarPosition = emptyScalarPosition;
9146
9131
  });
9147
9132
 
9148
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-node.js
9133
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-node.js
9149
9134
  var require_compose_node = __commonJS((exports) => {
9150
9135
  var Alias = require_Alias();
9151
9136
  var identity = require_identity();
@@ -9248,7 +9233,7 @@ var require_compose_node = __commonJS((exports) => {
9248
9233
  exports.composeNode = composeNode;
9249
9234
  });
9250
9235
 
9251
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-doc.js
9236
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-doc.js
9252
9237
  var require_compose_doc = __commonJS((exports) => {
9253
9238
  var Document = require_Document();
9254
9239
  var composeNode = require_compose_node();
@@ -9288,7 +9273,7 @@ var require_compose_doc = __commonJS((exports) => {
9288
9273
  exports.composeDoc = composeDoc;
9289
9274
  });
9290
9275
 
9291
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/composer.js
9276
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/composer.js
9292
9277
  var require_composer = __commonJS((exports) => {
9293
9278
  var node_process = __require("process");
9294
9279
  var directives = require_directives();
@@ -9479,7 +9464,7 @@ ${end.comment}` : end.comment;
9479
9464
  exports.Composer = Composer;
9480
9465
  });
9481
9466
 
9482
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-scalar.js
9467
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-scalar.js
9483
9468
  var require_cst_scalar = __commonJS((exports) => {
9484
9469
  var resolveBlockScalar = require_resolve_block_scalar();
9485
9470
  var resolveFlowScalar = require_resolve_flow_scalar();
@@ -9669,7 +9654,7 @@ var require_cst_scalar = __commonJS((exports) => {
9669
9654
  exports.setScalarValue = setScalarValue;
9670
9655
  });
9671
9656
 
9672
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-stringify.js
9657
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-stringify.js
9673
9658
  var require_cst_stringify = __commonJS((exports) => {
9674
9659
  var stringify = (cst) => ("type" in cst) ? stringifyToken(cst) : stringifyItem(cst);
9675
9660
  function stringifyToken(token) {
@@ -9727,7 +9712,7 @@ var require_cst_stringify = __commonJS((exports) => {
9727
9712
  exports.stringify = stringify;
9728
9713
  });
9729
9714
 
9730
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-visit.js
9715
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-visit.js
9731
9716
  var require_cst_visit = __commonJS((exports) => {
9732
9717
  var BREAK = Symbol("break visit");
9733
9718
  var SKIP = Symbol("skip children");
@@ -9786,7 +9771,7 @@ var require_cst_visit = __commonJS((exports) => {
9786
9771
  exports.visit = visit;
9787
9772
  });
9788
9773
 
9789
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst.js
9774
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst.js
9790
9775
  var require_cst = __commonJS((exports) => {
9791
9776
  var cstScalar = require_cst_scalar();
9792
9777
  var cstStringify = require_cst_stringify();
@@ -9887,7 +9872,7 @@ var require_cst = __commonJS((exports) => {
9887
9872
  exports.tokenType = tokenType;
9888
9873
  });
9889
9874
 
9890
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/lexer.js
9875
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/lexer.js
9891
9876
  var require_lexer = __commonJS((exports) => {
9892
9877
  var cst = require_cst();
9893
9878
  function isEmpty(ch) {
@@ -10484,7 +10469,7 @@ var require_lexer = __commonJS((exports) => {
10484
10469
  exports.Lexer = Lexer;
10485
10470
  });
10486
10471
 
10487
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/line-counter.js
10472
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/line-counter.js
10488
10473
  var require_line_counter = __commonJS((exports) => {
10489
10474
  class LineCounter {
10490
10475
  constructor() {
@@ -10512,7 +10497,7 @@ var require_line_counter = __commonJS((exports) => {
10512
10497
  exports.LineCounter = LineCounter;
10513
10498
  });
10514
10499
 
10515
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/parser.js
10500
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/parser.js
10516
10501
  var require_parser = __commonJS((exports) => {
10517
10502
  var node_process = __require("process");
10518
10503
  var cst = require_cst();
@@ -11368,7 +11353,7 @@ var require_parser = __commonJS((exports) => {
11368
11353
  exports.Parser = Parser;
11369
11354
  });
11370
11355
 
11371
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/public-api.js
11356
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/public-api.js
11372
11357
  var require_public_api = __commonJS((exports) => {
11373
11358
  var composer = require_composer();
11374
11359
  var Document = require_Document();
@@ -11462,7 +11447,7 @@ var require_public_api = __commonJS((exports) => {
11462
11447
  exports.stringify = stringify;
11463
11448
  });
11464
11449
 
11465
- // ../../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
11450
+ // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
11466
11451
  var composer, Document, Schema, errors2, Alias, identity, Pair, Scalar, YAMLMap, YAMLSeq, cst, lexer, lineCounter, parser, publicApi, visit, $Composer, $Document, $Schema, $YAMLError, $YAMLParseError, $YAMLWarning, $Alias, $isAlias, $isCollection, $isDocument, $isMap, $isNode, $isPair, $isScalar, $isSeq, $Pair, $Scalar, $YAMLMap, $YAMLSeq, $Lexer, $LineCounter, $Parser, $parse, $parseAllDocuments, $parseDocument, $stringify, $visit, $visitAsync;
11467
11452
  var init_dist = __esm(() => {
11468
11453
  composer = require_composer();
@@ -11511,7 +11496,7 @@ var init_dist = __esm(() => {
11511
11496
  $visitAsync = visit.visitAsync;
11512
11497
  });
11513
11498
 
11514
- // ../manifest/src/loader.ts
11499
+ // packages/libs/manifest/src/loader.ts
11515
11500
  import { createHash } from "crypto";
11516
11501
  import { readFile as readFile2, realpath as realpath2 } from "fs/promises";
11517
11502
  import path2 from "path";
@@ -11698,12 +11683,13 @@ var init_loader = __esm(() => {
11698
11683
  };
11699
11684
  });
11700
11685
 
11701
- // ../manifest/src/index.ts
11686
+ // packages/libs/manifest/src/index.ts
11702
11687
  var exports_src = {};
11703
11688
  __export(exports_src, {
11704
11689
  sandblocksManifestDigest: () => sandblocksManifestDigest,
11705
11690
  resolveServiceSteps: () => resolveServiceSteps,
11706
11691
  resolveProjectGraph: () => resolveProjectGraph,
11692
+ resolvePrebuiltImage: () => resolvePrebuiltImage,
11707
11693
  resolveEnvironment: () => resolveEnvironment,
11708
11694
  parseSandblocksManifest: () => parseSandblocksManifest,
11709
11695
  normalizeCommand: () => normalizeCommand,
@@ -11731,1693 +11717,2156 @@ var init_src = __esm(() => {
11731
11717
  init_loader();
11732
11718
  });
11733
11719
 
11734
- // src/cli.ts
11720
+ // packages/libs/cli/src/cli.ts
11735
11721
  init_src();
11736
11722
  import { randomUUID as randomUUID4 } from "crypto";
11737
- import { lstat, readFile as readFile8, stat as stat3, writeFile as writeFile5 } from "fs/promises";
11723
+ import { lstat, readFile as readFile8, readdir as readdir2, stat as stat3, writeFile as writeFile5 } from "fs/promises";
11738
11724
  import path6 from "path";
11739
11725
 
11740
- // src/hooks.ts
11741
- import { chmod, copyFile, mkdir, readFile as readFile3, rm, writeFile } from "fs/promises";
11742
- import path3 from "path";
11743
- var marker = "# sandblocks-managed-post-commit";
11744
- var backupName = "post-commit.before-sandblocks";
11745
- async function runHooksCommand(args, dependencies) {
11746
- const [command = "status", ...rest] = args;
11747
- if (command === "run") {
11748
- const [hook, directory] = rest;
11749
- if (hook !== "post-commit")
11750
- throw new Error("hooks run supports only post-commit");
11751
- return runPostCommit(path3.resolve(directory ?? process.cwd()), dependencies);
11726
+ // packages/libs/sdk/dist/index.js
11727
+ import { randomUUID } from "crypto";
11728
+ import { appendFile, cp, mkdir, readFile as readFile3, rm } from "fs/promises";
11729
+ import { dirname, resolve } from "path";
11730
+ import { spawn } from "child_process";
11731
+ import { access, mkdir as mkdir2, writeFile } from "fs/promises";
11732
+ import { resolve as resolve2 } from "path";
11733
+
11734
+ class CommandAgent {
11735
+ options;
11736
+ id;
11737
+ constructor(options) {
11738
+ this.options = options;
11739
+ this.id = options.id ?? "command";
11752
11740
  }
11753
- const root = path3.resolve(rest.find((value) => !value.startsWith("--")) ?? process.cwd());
11754
- if (command === "install")
11755
- return install(root);
11756
- if (command === "uninstall")
11757
- return uninstall(root);
11758
- if (command === "status")
11759
- return status(root);
11760
- throw new Error(`unsupported hooks command '${command}'`);
11761
- }
11762
- async function install(root) {
11763
- const hooksDir = await gitPath(root, "hooks");
11764
- const hookPath = path3.join(hooksDir, "post-commit");
11765
- const backupPath = path3.join(hooksDir, backupName);
11766
- await mkdir(hooksDir, { recursive: true });
11767
- const current = await text(hookPath);
11768
- if (current && !current.includes(marker)) {
11769
- await copyFile(hookPath, backupPath);
11770
- await chmod(backupPath, 493);
11741
+ async run(context) {
11742
+ const startedAt = new Date().toISOString();
11743
+ const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
11744
+ const promptMode = this.options.prompt ?? "stdin";
11745
+ const command = promptMode === "argument" ? [...base, context.prompt] : base;
11746
+ const idle = new AbortController;
11747
+ const completed = new AbortController;
11748
+ let idleTimer;
11749
+ let completionTimer;
11750
+ const resetIdle = () => {
11751
+ if (idleTimer)
11752
+ clearTimeout(idleTimer);
11753
+ idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
11754
+ };
11755
+ resetIdle();
11756
+ let output = "";
11757
+ let stdout = "";
11758
+ let stderr = "";
11759
+ let lineBuffer = "";
11760
+ let completionSignal;
11761
+ const consume = async (chunk, stream) => {
11762
+ resetIdle();
11763
+ output += chunk;
11764
+ if (stream === "stdout")
11765
+ stdout += chunk;
11766
+ else
11767
+ stderr += chunk;
11768
+ lineBuffer += chunk;
11769
+ const lines = lineBuffer.split(/\r?\n/);
11770
+ lineBuffer = lines.pop() ?? "";
11771
+ for (const line of lines)
11772
+ await this.emitLine(line, stream, context);
11773
+ completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
11774
+ if (completionSignal && !completionTimer) {
11775
+ completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
11776
+ }
11777
+ };
11778
+ try {
11779
+ const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
11780
+ try {
11781
+ const result = await context.runtime.exec(command, {
11782
+ env: this.options.env,
11783
+ stdin: promptMode === "stdin" ? context.prompt : undefined,
11784
+ signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
11785
+ onStdout: (chunk) => consume(chunk, "stdout"),
11786
+ onStderr: (chunk) => consume(chunk, "stderr")
11787
+ });
11788
+ stdout = result.stdout;
11789
+ stderr = result.stderr;
11790
+ return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
11791
+ } catch (error) {
11792
+ if (!completionSignal)
11793
+ throw error;
11794
+ return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
11795
+ }
11796
+ } finally {
11797
+ if (idleTimer)
11798
+ clearTimeout(idleTimer);
11799
+ if (completionTimer)
11800
+ clearTimeout(completionTimer);
11801
+ if (lineBuffer)
11802
+ await this.emitLine(lineBuffer, "stdout", context);
11803
+ }
11771
11804
  }
11772
- await writeFile(hookPath, `#!/bin/sh
11773
- ${marker}
11774
- set -u
11775
- previous="$(dirname "$0")/${backupName}"
11776
- if [ -x "$previous" ]; then "$previous" || exit $?; fi
11777
- root=$(git rev-parse --show-toplevel) || exit $?
11778
- cd "$root" || exit $?
11779
- if [ -x node_modules/.bin/sandblocks ]; then exec node_modules/.bin/sandblocks hooks run post-commit "$root"; fi
11780
- exec sandblocks hooks run post-commit "$root"
11781
- `);
11782
- await chmod(hookPath, 493);
11783
- console.log(`installed ${hookPath}`);
11784
- if (await exists(backupPath))
11785
- console.log(`chaining ${backupPath}`);
11786
- }
11787
- async function uninstall(root) {
11788
- const hooksDir = await gitPath(root, "hooks");
11789
- const hookPath = path3.join(hooksDir, "post-commit");
11790
- const backupPath = path3.join(hooksDir, backupName);
11791
- const current = await text(hookPath);
11792
- if (!current?.includes(marker))
11793
- throw new Error("Sandblocks post-commit hook is not installed");
11794
- if (await exists(backupPath)) {
11795
- await copyFile(backupPath, hookPath);
11796
- await chmod(hookPath, 493);
11797
- await rm(backupPath);
11798
- console.log(`restored ${hookPath}`);
11799
- } else {
11800
- await rm(hookPath);
11801
- console.log(`removed ${hookPath}`);
11805
+ result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
11806
+ const sessionId = this.options.sessionId?.(stdout, stderr);
11807
+ return {
11808
+ iteration: context.iteration,
11809
+ exitCode,
11810
+ stdout,
11811
+ stderr,
11812
+ ...sessionId ? { sessionId } : {},
11813
+ ...completionSignal ? { completionSignal } : {},
11814
+ startedAt,
11815
+ finishedAt: new Date().toISOString()
11816
+ };
11802
11817
  }
11803
- }
11804
- async function status(root) {
11805
- const hooksDir = await gitPath(root, "hooks");
11806
- const installed = (await text(path3.join(hooksDir, "post-commit")))?.includes(marker) === true;
11807
- const loaded = await loadPolicy(root);
11808
- console.log(`post-commit ${installed ? "installed" : "not installed"}`);
11809
- console.log(`policy ${loaded ? loaded.enabled ? "enabled" : "disabled" : "not configured"}`);
11810
- if (loaded) {
11811
- console.log(`branches ${loaded.branches.join(",")}`);
11812
- console.log(`environment ${loaded.environment}`);
11813
- console.log(`mode ${loaded.background ? "background" : "foreground"}`);
11818
+ async emitLine(line, stream, context) {
11819
+ const timestamp = new Date().toISOString();
11820
+ const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
11821
+ const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
11822
+ {
11823
+ type: "raw",
11824
+ line: stream === "stderr" ? `[stderr] ${line}` : line,
11825
+ iteration: context.iteration,
11826
+ timestamp
11827
+ }
11828
+ ];
11829
+ for (const event of events)
11830
+ await context.emit(event);
11814
11831
  }
11815
11832
  }
11816
- async function runPostCommit(root, dependencies) {
11817
- if (process.env.SANDBLOCKS_SKIP_POST_COMMIT === "1") {
11818
- console.log("sandblocks post-commit skipped by SANDBLOCKS_SKIP_POST_COMMIT");
11819
- return;
11820
- }
11821
- const policy = (await dependencies.load(root)).manifest.hooks.postCommit;
11822
- if (!policy?.enabled)
11823
- return;
11824
- const branch = await git(root, ["branch", "--show-current"]);
11825
- if (!policy.branches.includes(branch)) {
11826
- console.log(`sandblocks post-commit skipped on branch ${branch || "(detached)"}`);
11827
- return;
11833
+
11834
+ class ClaudeCodeAgent extends CommandAgent {
11835
+ constructor(options) {
11836
+ super({
11837
+ id: "claude-code",
11838
+ env: credentialEnvironment(options.env, options.credential),
11839
+ prompt: "argument",
11840
+ command: (context) => [
11841
+ options.binary ?? "claude",
11842
+ "--print",
11843
+ "--model",
11844
+ options.model,
11845
+ ...options.effort ? ["--effort", options.effort] : [],
11846
+ ...options.extraArgs ?? []
11847
+ ],
11848
+ sessionId: extractSessionId,
11849
+ parseEvent: parseJsonAgentEvent
11850
+ });
11828
11851
  }
11829
- const stateFile = path3.join(root, ".sandblocks", `sandbox-${policy.environment}.json`);
11830
- const command = await exists(stateFile) ? "redeploy" : "up";
11831
- const commandArgs = [root, "--environment", policy.environment];
11832
- if (policy.background) {
11833
- const logDir = path3.join(root, ".sandblocks", "logs");
11834
- await mkdir(logDir, { recursive: true });
11835
- const log = path3.join(logDir, `post-commit-${Date.now()}.log`);
11836
- const executable = process.argv[1];
11837
- if (!executable)
11838
- throw new Error("unable to resolve Sandblocks CLI executable");
11839
- const proc = Bun.spawn([process.execPath, executable, "sandbox", command, ...commandArgs], {
11840
- cwd: root,
11841
- stdout: Bun.file(log),
11842
- stderr: Bun.file(log),
11843
- env: { ...process.env, SANDBLOCKS_SKIP_POST_COMMIT: "1" }
11852
+ }
11853
+
11854
+ class CodexAgent extends CommandAgent {
11855
+ constructor(options) {
11856
+ super({
11857
+ id: "codex",
11858
+ env: credentialEnvironment(options.env, options.credential),
11859
+ prompt: "argument",
11860
+ command: (context) => [
11861
+ options.binary ?? "codex",
11862
+ "exec",
11863
+ "--model",
11864
+ options.model,
11865
+ ...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
11866
+ ...options.extraArgs ?? []
11867
+ ],
11868
+ sessionId: extractSessionId,
11869
+ parseEvent: parseJsonAgentEvent
11844
11870
  });
11845
- proc.unref();
11846
- console.log(`sandblocks ${command} started for ${policy.environment} (${log})`);
11847
- return;
11848
11871
  }
11849
- await dependencies.runSandbox(command, commandArgs);
11850
11872
  }
11851
- async function loadPolicy(root) {
11852
- const { loadSandblocksManifest: loadSandblocksManifest2 } = await Promise.resolve().then(() => (init_src(), exports_src));
11853
- return (await loadSandblocksManifest2({ repositoryRoot: root })).manifest.hooks.postCommit;
11873
+
11874
+ class PiAgent extends CommandAgent {
11875
+ constructor(options) {
11876
+ super({
11877
+ id: "pi",
11878
+ env: credentialEnvironment(options.env, options.credential),
11879
+ prompt: "argument",
11880
+ command: (context) => [
11881
+ options.binary ?? "pi",
11882
+ "--print",
11883
+ "--model",
11884
+ options.model,
11885
+ ...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
11886
+ ...options.extraArgs ?? []
11887
+ ],
11888
+ sessionId: extractSessionId,
11889
+ parseEvent: parseJsonAgentEvent
11890
+ });
11891
+ }
11854
11892
  }
11855
- async function gitPath(root, name) {
11856
- return path3.resolve(root, await git(root, ["rev-parse", "--git-path", name]));
11857
- }
11858
- async function git(root, args) {
11859
- const proc = Bun.spawn(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
11860
- const [stdout, stderr, code] = await Promise.all([
11861
- new Response(proc.stdout).text(),
11862
- new Response(proc.stderr).text(),
11863
- proc.exited
11864
- ]);
11865
- if (code !== 0)
11866
- throw new Error(stderr.trim() || `git ${args.join(" ")} failed`);
11867
- return stdout.trim();
11868
- }
11869
- async function exists(file) {
11870
- return Bun.file(file).exists();
11871
- }
11872
- async function text(file) {
11893
+ function parseJsonAgentEvent(line, context) {
11894
+ let value;
11873
11895
  try {
11874
- return await readFile3(file, "utf8");
11896
+ value = JSON.parse(line);
11875
11897
  } catch {
11876
- return;
11877
- }
11878
- }
11879
-
11880
- // src/runtime.ts
11881
- import { chmod as chmod2, mkdir as mkdir2, readdir, rm as rm2 } from "fs/promises";
11882
- import path4 from "path";
11883
- var RUNTIME_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
11884
- var NODE_ENV_RUNTIME = {
11885
- production: "prod",
11886
- development: "dev",
11887
- test: "local"
11888
- };
11889
- async function runRuntimeCommand(args) {
11890
- const [action, ...rest] = args;
11891
- if (action !== "prepare")
11892
- throw new Error("runtime action must be prepare");
11893
- const options = parse(rest);
11894
- const root = path4.resolve(options.positionals[0] ?? process.cwd());
11895
- const runtime = normalizedRuntime(option(options, "runtime") ?? process.env.SANDBLOCKS_RUNTIME ?? "development" ?? "development");
11896
- const targetRoot = contained(root, option(options, "target") ?? ".locker", "runtime target");
11897
- const runtimeRoot = path4.join(targetRoot, runtime);
11898
- const config = contained(root, option(options, "config") ?? "config/dotlocker.config.ts", "Dotlocker config");
11899
- const dotlocker = path4.resolve(root, option(options, "dotlocker") ?? path4.join("node_modules", ".bin", "dotlocker"));
11900
- if (!await Bun.file(config).exists())
11901
- throw new Error(`Dotlocker config does not exist: ${config}`);
11902
- if (!await Bun.file(dotlocker).exists()) {
11903
- throw new Error("Dotlocker is not installed; add @dotlocker/dotlocker to the workspace");
11898
+ return { type: "raw", line, ...context };
11904
11899
  }
11905
- await rm2(targetRoot, { recursive: true, force: true });
11906
- await mkdir2(runtimeRoot, { recursive: true, mode: 493 });
11907
- const child = Bun.spawn([dotlocker, "pull", "--config", config, "--runtime", runtime, "--out", runtimeRoot], { cwd: root, env: process.env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
11908
- const exitCode = await child.exited;
11909
- if (exitCode !== 0)
11910
- throw new Error(`Dotlocker pull failed with exit code ${exitCode}`);
11911
- const envFile = path4.join(runtimeRoot, ".env");
11912
- if (!await Bun.file(envFile).exists()) {
11913
- throw new Error(`Dotlocker did not materialize ${path4.relative(root, envFile)}`);
11900
+ const type = String(value.type ?? "");
11901
+ if (/tool/i.test(type)) {
11902
+ return {
11903
+ type: "toolCall",
11904
+ name: String(value.name ?? value.tool ?? value.tool_name ?? "tool"),
11905
+ ...value.input !== undefined ? { input: value.input } : {},
11906
+ ...context
11907
+ };
11914
11908
  }
11915
- await normalizePermissions(targetRoot);
11916
- console.log(`runtime ${runtime}
11917
- environment ${path4.relative(root, envFile)}`);
11909
+ const text = value.text ?? value.message ?? value.content;
11910
+ return typeof text === "string" ? { type: "text", text, ...context } : { type: "raw", line, ...context };
11918
11911
  }
11919
- function normalizedRuntime(value) {
11920
- const runtime = NODE_ENV_RUNTIME[value.trim()] ?? value.trim();
11921
- if (!RUNTIME_PATTERN.test(runtime))
11922
- throw new Error(`invalid runtime '${value}'`);
11923
- return runtime;
11912
+ function credentialEnvironment(env, credential) {
11913
+ if (!credential)
11914
+ return env;
11915
+ if (!/^[A-Z][A-Z0-9_]*$/.test(credential.environment))
11916
+ throw new Error("credential environment name is invalid");
11917
+ const value = process.env[credential.environment];
11918
+ if (value === undefined)
11919
+ return env;
11920
+ const target = credential.target ?? credential.environment;
11921
+ if (!/^[A-Z][A-Z0-9_]*$/.test(target))
11922
+ throw new Error("credential target name is invalid");
11923
+ return { ...env, [target]: value };
11924
11924
  }
11925
- function contained(root, value, label) {
11926
- const resolved = path4.resolve(root, value);
11927
- const relative = path4.relative(root, resolved);
11928
- if (!relative || !relative.startsWith("..") && !path4.isAbsolute(relative))
11929
- return resolved;
11930
- throw new Error(`${label} must remain within the repository`);
11925
+ function extractSessionId(stdout, stderr) {
11926
+ const match = `${stdout}
11927
+ ${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
11928
+ return match?.[1];
11931
11929
  }
11932
- async function normalizePermissions(directory) {
11933
- await chmod2(directory, 493);
11934
- for (const entry of await readdir(directory, { withFileTypes: true })) {
11935
- const child = path4.join(directory, entry.name);
11936
- if (entry.isSymbolicLink())
11937
- throw new Error("Dotlocker output must not contain symbolic links");
11938
- if (entry.isDirectory())
11939
- await normalizePermissions(child);
11940
- else if (entry.isFile())
11941
- await chmod2(child, 420);
11942
- else
11943
- throw new Error("Dotlocker output contains an unsupported file type");
11930
+ var Output = {
11931
+ string(input) {
11932
+ return { kind: "string", tag: validTag(input.tag) };
11933
+ },
11934
+ object(input) {
11935
+ return { kind: "object", tag: validTag(input.tag), schema: input.schema };
11944
11936
  }
11945
- }
11946
- function parse(args) {
11947
- const options = { positionals: [] };
11948
- for (let index = 0;index < args.length; index += 1) {
11949
- const value = args[index] ?? "";
11950
- if (!value.startsWith("--")) {
11951
- options.positionals.push(value);
11952
- continue;
11953
- }
11954
- const key = value.slice(2);
11955
- const next = args[index + 1];
11956
- if (next && !next.startsWith("--")) {
11957
- options[key] = next;
11958
- index += 1;
11959
- } else
11960
- options[key] = true;
11937
+ };
11938
+ async function extractOutput(spec, text) {
11939
+ const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
11940
+ const match = text.match(pattern);
11941
+ if (!match)
11942
+ throw new Error(`structured output tag <${spec.tag}> was not found`);
11943
+ const content = match[1]?.trim();
11944
+ if (content === undefined)
11945
+ throw new Error(`structured output tag <${spec.tag}> is empty`);
11946
+ if (spec.kind === "string")
11947
+ return content;
11948
+ let value;
11949
+ try {
11950
+ value = JSON.parse(content);
11951
+ } catch {
11952
+ throw new Error(`structured output <${spec.tag}> is not valid JSON`);
11961
11953
  }
11962
- return options;
11963
- }
11964
- function option(options, key) {
11965
- const value = options[key];
11966
- return typeof value === "string" ? value : undefined;
11954
+ const schema = spec.schema;
11955
+ if (schema["~standard"]) {
11956
+ const result = await schema["~standard"].validate(value);
11957
+ if (result.issues?.length)
11958
+ throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
11959
+ return result.value;
11960
+ }
11961
+ if (schema.safeParse) {
11962
+ const result = schema.safeParse(value);
11963
+ if (!result.success)
11964
+ throw new Error(`structured output validation failed: ${String(result.error)}`);
11965
+ return result.data;
11966
+ }
11967
+ if (schema.parse)
11968
+ return schema.parse(value);
11969
+ throw new Error("structured output schema is unsupported");
11967
11970
  }
11968
-
11969
- // src/sandbox.ts
11970
- init_src();
11971
- import { createHash as createHash2, randomUUID } from "crypto";
11972
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rename, rm as rm3 } from "fs/promises";
11973
- import path5 from "path";
11974
- var COMMANDS = new Set([
11975
- "up",
11976
- "create",
11977
- "status",
11978
- "deploy",
11979
- "redeploy",
11980
- "renew",
11981
- "exec",
11982
- "git",
11983
- "down",
11984
- "destroy"
11985
- ]);
11986
- function isStatefulSandboxCommand(command) {
11987
- return Boolean(command && COMMANDS.has(command));
11971
+ function validTag(tag) {
11972
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tag))
11973
+ throw new Error("output tag is invalid");
11974
+ return tag;
11988
11975
  }
11989
- async function runStatefulSandboxCommand(command, args, dependencies) {
11990
- if (command === "up")
11991
- return up(args, dependencies);
11992
- if (command === "create")
11993
- return create(args, dependencies);
11994
- if (command === "status")
11995
- return status2(args, dependencies);
11996
- if (command === "deploy")
11997
- return deploy(args, dependencies);
11998
- if (command === "redeploy")
11999
- return redeploy(args, dependencies);
12000
- if (command === "renew")
12001
- return renew(args, dependencies);
12002
- if (command === "exec")
12003
- return exec(args, dependencies);
12004
- if (command === "git")
12005
- return git2(args, dependencies);
12006
- return down(args, dependencies);
11976
+ function escapeRegex(value) {
11977
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12007
11978
  }
12008
- async function up(args, dependencies) {
12009
- await withDeploymentLock(args, dependencies, async () => {
12010
- await create(args, dependencies);
12011
- try {
12012
- await deployUnlocked(args, dependencies);
12013
- } catch (error) {
12014
- if (process.env.SANDBLOCKS_RETAIN_FAILED !== "true") {
12015
- await down(args, dependencies).catch(() => {
12016
- return;
12017
- });
12018
- }
12019
- throw error;
12020
- }
11979
+ async function executeProcess(command, options = {}) {
11980
+ if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
11981
+ throw new Error("command must contain safe non-empty arguments");
11982
+ }
11983
+ const startedAt = new Date().toISOString();
11984
+ const [executable, ...args] = command;
11985
+ if (!executable)
11986
+ throw new Error("command executable is required");
11987
+ const child = spawn(executable, args, {
11988
+ cwd: options.cwd ?? options.hostCwd,
11989
+ env: options.env ? { ...process.env, ...options.env } : process.env,
11990
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
12021
11991
  });
12022
- }
12023
- async function create(args, dependencies) {
12024
- const options = dependencies.parse(args);
12025
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12026
- await loadLocalEnvironment(root);
12027
- const environment = dependencies.option(options, "environment") ?? "preview";
12028
- const file = statePath(root, environment);
12029
- if (await Bun.file(file).exists())
12030
- throw new Error(`sandbox '${environment}' already exists`);
12031
- const loaded = await dependencies.load(root, dependencies.option(options, "config") ?? dependencies.option(options, "manifest"));
12032
- const preview = selectPreview(loaded, environment);
12033
- const projectId = dependencies.option(options, "project") ?? process.env.SANDBLOCKS_PROJECT_ID ?? "default";
12034
- const repository = loaded.manifest.name ?? path5.basename(root).toLowerCase();
12035
- const sandboxId = randomUUID();
12036
- const workspaceId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${repository}`.slice(0, 63);
12037
- const { apiUrl, apiKey } = client(options, dependencies.option, projectId);
12038
- await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes`, {
12039
- method: "POST",
12040
- body: JSON.stringify({ id: sandboxId, goal: preview.goal, status: "active", repositories: [repository] })
11992
+ if (options.stdin !== undefined)
11993
+ child.stdin?.end(options.stdin);
11994
+ let timedOut = false;
11995
+ let stdout = "";
11996
+ let stderr = "";
11997
+ let callbackQueue = Promise.resolve();
11998
+ child.stdout?.on("data", (chunk) => {
11999
+ const text = chunk.toString();
12000
+ stdout = `${stdout}${text}`.slice(-4 * 1024 * 1024);
12001
+ callbackQueue = callbackQueue.then(() => options.onStdout?.(text)).then(() => {
12002
+ return;
12003
+ });
12041
12004
  });
12042
- try {
12043
- const lease = await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}/leases`, {
12044
- method: "POST",
12045
- body: JSON.stringify({
12046
- subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
12047
- scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
12048
- ttlSeconds: Number(dependencies.option(options, "ttl-seconds") ?? 28800)
12049
- })
12005
+ child.stderr?.on("data", (chunk) => {
12006
+ const text = chunk.toString();
12007
+ stderr = `${stderr}${text}`.slice(-4 * 1024 * 1024);
12008
+ callbackQueue = callbackQueue.then(() => options.onStderr?.(text)).then(() => {
12009
+ return;
12050
12010
  });
12051
- const imported = await importSource({
12052
- root,
12053
- projectId,
12054
- sandboxId,
12055
- workspaceId,
12056
- pool: dependencies.option(options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
12057
- apiUrl,
12058
- apiKey,
12059
- idempotencyKey: `${sandboxId}:workspace-import`,
12060
- source: dependencies.source,
12061
- ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
12011
+ });
12012
+ const stop = () => child.kill("SIGTERM");
12013
+ options.signal?.addEventListener("abort", stop, { once: true });
12014
+ const timer = options.timeoutMs ? setTimeout(() => {
12015
+ timedOut = true;
12016
+ child.kill("SIGKILL");
12017
+ }, options.timeoutMs) : undefined;
12018
+ try {
12019
+ const exitCode = await new Promise((resolve3, reject) => {
12020
+ child.once("error", reject);
12021
+ child.once("close", (code, signal) => resolve3(code ?? (signal ? 128 : 1)));
12062
12022
  });
12063
- const hostId = String(imported.operation.assignedHostId ?? "");
12064
- if (!hostId)
12065
- throw new Error("workspace import did not bind a sandbox host");
12066
- const state = {
12067
- version: 1,
12068
- environment,
12069
- projectId,
12070
- sandboxId,
12071
- workspaceId,
12072
- repository,
12073
- leaseId: lease.lease.id,
12074
- leaseToken: lease.token,
12075
- hostId,
12076
- sequence: 0,
12077
- createdAt: new Date().toISOString()
12023
+ await callbackQueue;
12024
+ if (options.signal?.aborted)
12025
+ throw options.signal.reason ?? new Error("operation aborted");
12026
+ return {
12027
+ exitCode: timedOut ? 124 : exitCode,
12028
+ stdout,
12029
+ stderr,
12030
+ timedOut,
12031
+ startedAt,
12032
+ finishedAt: new Date().toISOString()
12078
12033
  };
12079
- await writeState(file, state);
12080
- console.log(`sandbox ${sandboxId}`);
12081
- console.log(`workspace ${workspaceId}`);
12082
- console.log(`host ${hostId}`);
12083
- } catch (error) {
12084
- await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
12085
- method: "DELETE"
12086
- }).catch(() => {
12087
- return;
12088
- });
12089
- throw error;
12034
+ } finally {
12035
+ if (timer)
12036
+ clearTimeout(timer);
12037
+ options.signal?.removeEventListener("abort", stop);
12090
12038
  }
12091
12039
  }
12092
- async function status2(args, dependencies) {
12093
- const context = await getContext(args, dependencies);
12094
- const [sandbox, session] = await Promise.all([
12095
- request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`),
12096
- leaseRequest(context, "/v1/sandbox-session")
12097
- ]);
12098
- if (context.options.json) {
12099
- console.log(JSON.stringify({ state: redactedState(context.state), sandbox: sandbox.sandbox, session: session.session }, null, 2));
12100
- return;
12040
+ class SandblocksSandbox {
12041
+ runtime;
12042
+ options;
12043
+ git;
12044
+ id;
12045
+ provider;
12046
+ cwd;
12047
+ failed = false;
12048
+ disposed = false;
12049
+ eventQueue = [];
12050
+ eventWaiters = [];
12051
+ constructor(runtime, options, git) {
12052
+ this.runtime = runtime;
12053
+ this.options = options;
12054
+ this.git = git;
12055
+ this.id = runtime.id;
12056
+ this.provider = runtime.provider;
12057
+ this.cwd = runtime.cwd;
12101
12058
  }
12102
- console.log(`state ${path5.relative(context.root, context.file)}`);
12103
- console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
12104
- console.log(`workspace ${context.state.workspaceId}`);
12105
- console.log(`host ${context.state.hostId}`);
12106
- console.log(`lease expires ${session.session.expiresAt}`);
12107
- for (const route of context.state.previewUrls ?? [])
12108
- console.log(`${route.service ?? "service"} ${route.url}`);
12109
- }
12110
- async function deploy(args, dependencies) {
12111
- await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
12112
- }
12113
- async function withDeploymentLock(args, dependencies, action) {
12114
- const options = dependencies.parse(args);
12115
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12116
- const environment = dependencies.option(options, "environment") ?? "preview";
12117
- const lock = `${statePath(root, environment)}.deploy.lock`;
12118
- await mkdir3(path5.dirname(lock), { recursive: true, mode: 448 });
12119
- try {
12120
- await mkdir3(lock);
12121
- } catch (error) {
12122
- if (error.code === "EEXIST") {
12123
- throw new Error(`sandbox '${environment}' deployment is already running`);
12124
- }
12125
- throw error;
12059
+ exec(command, options = {}) {
12060
+ return this.runtime.exec(command, options);
12126
12061
  }
12127
- try {
12128
- await action();
12129
- } finally {
12130
- await rm3(lock, { recursive: true, force: true });
12062
+ upload(source, destination) {
12063
+ if (!this.runtime.upload)
12064
+ throw new Error(`${this.provider} provider does not support upload`);
12065
+ return this.runtime.upload(source, destination);
12131
12066
  }
12132
- }
12133
- async function deployUnlocked(args, dependencies) {
12134
- const context = await getContext(args, dependencies);
12135
- if (context.state.deploymentId)
12136
- throw new Error("preview is already deployed; use sandbox redeploy");
12137
- const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
12138
- const stack = loaded.manifest.name ?? context.state.repository;
12139
- const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
12140
- const targetHost = await resolveAssignedTargetHost(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
12141
- if (!targetHost)
12142
- throw new Error("assigned host targetHost label or preview targetHost is required");
12143
- const deploymentId = randomUUID();
12144
- const environmentSnapshotId = await createEnvironmentSnapshot(context);
12145
- const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
12146
- const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
12147
- if (serviceSteps.length) {
12148
- const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12149
- method: "POST",
12150
- headers: {
12151
- "idempotency-key": `preview:${deploymentId}:steps`
12152
- },
12153
- body: JSON.stringify({
12154
- kind: "workspace.step.run",
12155
- stack: context.state.repository,
12156
- environmentId: context.state.environment,
12157
- environmentSnapshotId,
12158
- payload: {
12159
- workspaceId: context.state.workspaceId,
12160
- sandboxId: context.state.sandboxId,
12161
- revisionId: deploymentId,
12162
- runtimeType: preview.deploymentType,
12163
- serviceUrls,
12164
- steps: serviceSteps,
12165
- placement: { hostId: context.state.hostId }
12166
- },
12167
- maxAttempts: 1
12168
- })
12169
- });
12170
- await wait(context.apiUrl, context.apiKey, stepOperation.operation.id, 60 * 60000);
12067
+ download(source, destination) {
12068
+ if (!this.runtime.download)
12069
+ throw new Error(`${this.provider} provider does not support download`);
12070
+ return this.runtime.download(source, destination);
12171
12071
  }
12172
- const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12173
- method: "POST",
12174
- headers: {
12175
- "idempotency-key": `preview:${deploymentId}:deploy`
12176
- },
12177
- body: JSON.stringify({
12178
- kind: "workspace.service.deploy",
12179
- stack: context.state.repository,
12180
- environmentId: context.state.environment,
12181
- environmentSnapshotId,
12182
- payload: {
12183
- workspaceId: context.state.workspaceId,
12184
- deploymentId,
12185
- sandboxId: context.state.sandboxId,
12186
- runtimeType: preview.deploymentType,
12187
- stack,
12188
- workspace: preview.workspace,
12189
- targetHost,
12190
- serviceUrls,
12191
- services: preview.services,
12192
- placement: { hostId: context.state.hostId }
12193
- },
12194
- maxAttempts: 1
12195
- })
12196
- });
12197
- const operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
12198
- context.state.deploymentId = deploymentId;
12199
- await writeState(context.file, context.state);
12200
- const routes = operation.result?.serviceRoutes ?? [];
12201
- const images = operation.result?.images ?? [];
12202
- if (typeof preview.ssoEnabled === "boolean") {
12203
- 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 }) });
12072
+ stop(reason) {
12073
+ return this.runtime.stop(reason);
12204
12074
  }
12205
- let sandbox = await recordSandbox(context, loaded, preview, routes, operation.result?.healthEvidence ?? [], images, preview.steps.length ? "active" : "available");
12206
- let checkEvidence = [];
12207
- try {
12208
- if (preview.steps.length) {
12209
- const checkRoutes = sandbox.previewUrls ?? [];
12210
- await waitForPublishedRoutes(checkRoutes);
12211
- const serviceUrls2 = Object.fromEntries(checkRoutes.flatMap((route) => route.service ? [
12212
- [
12213
- `SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`,
12214
- route.url
12215
- ]
12216
- ] : []));
12217
- const check = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12218
- method: "POST",
12219
- headers: {
12220
- "idempotency-key": `preview:${deploymentId}:checks`
12221
- },
12222
- body: JSON.stringify({
12223
- kind: "workspace.check.run",
12224
- stack: context.state.repository,
12225
- environmentId: context.state.environment,
12226
- environmentSnapshotId,
12227
- payload: {
12228
- workspaceId: context.state.workspaceId,
12229
- deploymentId,
12230
- sandboxId: context.state.sandboxId,
12231
- runtimeType: preview.deploymentType,
12232
- checks: preview.steps,
12233
- serviceUrls: serviceUrls2,
12234
- placement: { hostId: context.state.hostId }
12235
- },
12236
- maxAttempts: 1
12237
- })
12238
- });
12239
- const checked = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
12240
- checkEvidence = checked.result?.checks ?? [];
12241
- if (checked.result?.passed !== true) {
12242
- throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
12075
+ async* events(options = {}) {
12076
+ while (!this.disposed || this.eventQueue.length) {
12077
+ if (options.signal?.aborted)
12078
+ return;
12079
+ const queued = this.eventQueue.shift();
12080
+ if (queued) {
12081
+ yield queued;
12082
+ continue;
12243
12083
  }
12244
- sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
12084
+ const event = await new Promise((resolve22) => {
12085
+ const abort = () => resolve22(undefined);
12086
+ options.signal?.addEventListener("abort", abort, { once: true });
12087
+ this.eventWaiters.push((value) => {
12088
+ options.signal?.removeEventListener("abort", abort);
12089
+ resolve22(value);
12090
+ });
12091
+ });
12092
+ if (!event)
12093
+ return;
12094
+ yield event;
12245
12095
  }
12246
- } catch (error) {
12247
- await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
12248
- return;
12249
- });
12250
- await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
12251
- return;
12252
- });
12253
- throw error;
12254
12096
  }
12255
- await releaseEnvironmentSnapshot(context, environmentSnapshotId);
12256
- context.state.deploymentId = deploymentId;
12257
- const previewUrls = sandbox.previewUrls ?? [];
12258
- context.state.previewUrls = previewUrls;
12259
- await writeState(context.file, context.state);
12260
- console.log(`deployment ${deploymentId} available`);
12261
- for (const route of previewUrls)
12262
- console.log(`${route.service ?? "service"} ${route.url}`);
12263
- }
12264
- async function redeploy(args, dependencies) {
12265
- const context = await getContext(args, dependencies);
12266
- await renewLease(context, dependencies);
12267
- const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
12268
- const previousDeploymentId = context.state.deploymentId;
12269
- const previousUrls = context.state.previewUrls;
12270
- const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...candidateState } = context.state;
12271
- context.state = candidateState;
12272
- await writeState(context.file, context.state);
12273
- try {
12274
- await destroyWorkspace(context, `${context.state.sandboxId}:workspace-redeploy-destroy:${Date.now()}`);
12275
- const imported = await importSource({
12276
- root: context.root,
12277
- projectId: context.state.projectId,
12278
- sandboxId: context.state.sandboxId,
12279
- workspaceId: context.state.workspaceId,
12280
- pool: dependencies.option(context.options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
12281
- apiUrl: context.apiUrl,
12282
- apiKey: context.apiKey,
12283
- idempotencyKey: `${context.state.sandboxId}:workspace-redeploy:${Date.now()}`,
12284
- source: dependencies.source,
12285
- ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
12286
- });
12287
- const hostId = String(imported.operation.assignedHostId ?? "");
12288
- if (!hostId)
12289
- throw new Error("workspace re-import did not bind a sandbox host");
12290
- context.state.hostId = hostId;
12291
- await writeState(context.file, context.state);
12292
- await deploy(args, dependencies);
12293
- if (previousDeploymentId)
12294
- await destroyDeploymentById(context, previousDeploymentId);
12295
- } catch (error) {
12296
- const failedCandidateId = context.state.deploymentId;
12297
- if (failedCandidateId && failedCandidateId !== previousDeploymentId) {
12298
- await destroyDeploymentById(context, failedCandidateId).catch(() => {
12299
- return;
12300
- });
12097
+ async run(options) {
12098
+ if (options.output && (options.maxIterations ?? 1) !== 1)
12099
+ throw new Error("structured output requires maxIterations === 1");
12100
+ const startedAt = new Date().toISOString();
12101
+ const prompt = await resolvePrompt(options);
12102
+ const signals = array(options.completionSignal ?? "<promise>COMPLETE</promise>");
12103
+ const logger = createLogger(options.logging, this.git.hostCwd);
12104
+ const baseline = await gitHead(this.git.runtimeCwd);
12105
+ const iterations = [];
12106
+ try {
12107
+ await runSandboxHooks(this.runtime, options.hooks?.sandbox?.beforeAgent);
12108
+ for (let iteration = 1;iteration <= (options.maxIterations ?? 1); iteration += 1) {
12109
+ const result = await options.agent.run({
12110
+ runtime: this.runtime,
12111
+ prompt,
12112
+ iteration,
12113
+ signal: options.signal,
12114
+ idleTimeoutSeconds: options.idleTimeoutSeconds ?? 600,
12115
+ completionTimeoutSeconds: options.completionTimeoutSeconds ?? 60,
12116
+ completionSignal: signals,
12117
+ emit: async (event) => {
12118
+ this.publish(event);
12119
+ await logger(event);
12120
+ }
12121
+ });
12122
+ iterations.push(result);
12123
+ if (result.exitCode !== 0)
12124
+ throw new Error(result.stderr || `${options.agent.id} exited ${result.exitCode}`);
12125
+ if (result.completionSignal)
12126
+ break;
12127
+ }
12128
+ await runSandboxHooks(this.runtime, options.hooks?.sandbox?.afterAgent);
12129
+ await this.runtime.syncToHost?.();
12130
+ const commits = await collectCommits(this.git.runtimeCwd, baseline, this.options.timeouts?.commitCollectionMs);
12131
+ if (this.git.mergeToHead && this.git.branch)
12132
+ await mergeBranch(this.git, this.options.timeouts?.mergeToHostMs);
12133
+ const allOutput = iterations.map((item) => item.stdout).join(`
12134
+ `);
12135
+ const completionSignal = iterations.find((item) => item.completionSignal)?.completionSignal;
12136
+ return {
12137
+ sandboxId: this.id,
12138
+ provider: this.provider,
12139
+ iterations,
12140
+ commits,
12141
+ ...this.git.branch ? { branch: this.git.branch } : {},
12142
+ ...completionSignal ? { completionSignal } : {},
12143
+ ...options.output ? { output: await extractOutput(options.output, allOutput) } : {},
12144
+ startedAt,
12145
+ finishedAt: new Date().toISOString()
12146
+ };
12147
+ } catch (error) {
12148
+ this.failed = true;
12149
+ throw error;
12301
12150
  }
12302
- context.state.deploymentId = previousDeploymentId;
12303
- context.state.previewUrls = previousUrls;
12304
- await writeState(context.file, context.state);
12305
- throw error;
12151
+ }
12152
+ async destroy() {
12153
+ if (this.disposed)
12154
+ return;
12155
+ this.disposed = true;
12156
+ await runHostHooks(this.options.hooks?.host?.beforeDestroy, this.git.hostCwd);
12157
+ const retention = this.options.retention ?? "destroy";
12158
+ if (retention === "destroy" || retention === "on-failure" && !this.failed)
12159
+ await this.runtime.destroy();
12160
+ for (const waiter of this.eventWaiters.splice(0))
12161
+ waiter(undefined);
12162
+ if (this.git.worktree && retention !== "always") {
12163
+ await executeProcess([
12164
+ "git",
12165
+ "-C",
12166
+ this.git.hostCwd,
12167
+ "worktree",
12168
+ "remove",
12169
+ "--force",
12170
+ this.git.worktree
12171
+ ]);
12172
+ await rm(this.git.worktree, { recursive: true, force: true });
12173
+ }
12174
+ }
12175
+ async[Symbol.asyncDispose]() {
12176
+ await this.destroy();
12177
+ }
12178
+ publish(event) {
12179
+ const waiter = this.eventWaiters.shift();
12180
+ if (waiter)
12181
+ waiter(event);
12182
+ else
12183
+ this.eventQueue.push(event);
12306
12184
  }
12307
12185
  }
12308
- async function renew(args, dependencies) {
12309
- const context = await getContext(args, dependencies);
12310
- const lease = await renewLease(context, dependencies);
12311
- console.log(`lease ${lease.lease.id} renewed until ${lease.lease.expiresAt}`);
12312
- }
12313
- async function renewLease(context, dependencies) {
12314
- const previousLeaseId = context.state.leaseId;
12315
- const lease = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}/leases`, {
12316
- method: "POST",
12317
- body: JSON.stringify({
12318
- subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
12319
- scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
12320
- ttlSeconds: Number(dependencies.option(context.options, "ttl-seconds") ?? 28800)
12321
- })
12186
+ async function createSandbox(options) {
12187
+ const provider = options.provider ?? options.sandbox;
12188
+ if (!provider)
12189
+ throw new Error("sandbox provider is required");
12190
+ assertProviderCapabilities(provider, options);
12191
+ const hostCwd = resolve(options.cwd ?? process.cwd());
12192
+ await runHostHooks(options.hooks?.host?.beforeCreate, hostCwd);
12193
+ const branchStrategy = options.branch ?? options.branchStrategy ?? { type: "head" };
12194
+ if (options.copyToWorktree?.length && branchStrategy.type === "head") {
12195
+ throw new Error("copyToWorktree is not supported with the head branch strategy");
12196
+ }
12197
+ const git = await prepareGit(hostCwd, branchStrategy, options.timeouts?.gitSetupMs);
12198
+ await runHostHooks(options.hooks?.host?.onWorktreeReady, git.runtimeCwd);
12199
+ for (const file of options.copyToWorktree ?? []) {
12200
+ const source = resolve(hostCwd, file);
12201
+ const destination = resolve(git.runtimeCwd, file);
12202
+ await mkdir(dirname(destination), { recursive: true });
12203
+ await withTimeout(cp(source, destination, { recursive: true }), options.timeouts?.copyToWorktreeMs ?? 60000, `copying '${file}'`);
12204
+ }
12205
+ const id2 = options.id ?? randomUUID();
12206
+ const runtime = await provider.create({
12207
+ id: id2,
12208
+ cwd: git.runtimeCwd,
12209
+ image: options.image,
12210
+ env: options.env,
12211
+ mounts: options.mounts,
12212
+ network: options.network,
12213
+ groups: options.groups,
12214
+ devices: options.devices,
12215
+ cpus: options.cpus,
12216
+ memoryMb: options.memoryMb,
12217
+ pids: options.pids,
12218
+ tmpfsMb: options.tmpfsMb,
12219
+ signal: options.signal,
12220
+ idempotencyKey: options.idempotencyKey,
12221
+ metadata: options.metadata
12322
12222
  });
12323
- context.state.leaseId = lease.lease.id;
12324
- context.state.leaseToken = lease.token;
12325
- await writeState(context.file, context.state);
12326
- if (previousLeaseId && previousLeaseId !== lease.lease.id) {
12327
- await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}/leases/${previousLeaseId}`, { method: "DELETE" }).catch(() => {
12223
+ const sandbox = new SandblocksSandbox(runtime, options, git);
12224
+ try {
12225
+ await runHostHooks(options.hooks?.host?.onSandboxReady, hostCwd);
12226
+ await runHostHooks(options.hooks?.host?.afterCreate, hostCwd);
12227
+ await runSandboxHooks(runtime, options.hooks?.sandbox?.onSandboxReady);
12228
+ await runSandboxHooks(runtime, options.hooks?.sandbox?.afterCreate);
12229
+ return sandbox;
12230
+ } catch (error) {
12231
+ await sandbox.destroy().catch(() => {
12328
12232
  return;
12329
12233
  });
12234
+ throw error;
12330
12235
  }
12331
- return lease;
12332
12236
  }
12333
- async function exec(args, dependencies) {
12334
- const separator = args.indexOf("--");
12335
- if (separator < 0 || separator === args.length - 1)
12336
- throw new Error("sandbox exec requires -- followed by a command");
12337
- const context = await getContext(args.slice(0, separator), dependencies);
12338
- const command = args.slice(separator + 1);
12339
- const operation = await dataOperation(context, "exec", { executable: command[0], args: command.slice(1) });
12340
- printOutput(operation);
12237
+ async function connectSandbox(options) {
12238
+ if (!options.provider.reconnect)
12239
+ throw new Error(`${options.provider.kind} provider does not support reconnect`);
12240
+ const cwd = resolve(options.cwd ?? process.cwd());
12241
+ const runtime = await options.provider.reconnect({ id: options.id, cwd });
12242
+ return new SandblocksSandbox(runtime, { provider: options.provider, cwd, retention: options.retention ?? "always" }, { hostCwd: cwd, runtimeCwd: cwd });
12341
12243
  }
12342
- async function git2(args, dependencies) {
12343
- const actionIndex = args.findIndex((value) => ["status", "diff", "stage", "checkout"].includes(value));
12344
- if (actionIndex < 0)
12345
- throw new Error("sandbox git action must be status, diff, stage, or checkout");
12346
- const context = await getContext(args.slice(0, actionIndex), dependencies);
12347
- const action = args[actionIndex];
12348
- if (!action)
12349
- throw new Error("sandbox git action is required");
12350
- const rest = args.slice(actionIndex + 1);
12351
- const operation = await dataOperation(context, "git", action === "checkout" ? { action, branch: rest[0] } : { action, ...rest.length ? { paths: rest } : {} });
12352
- printOutput(operation);
12244
+ async function run(options) {
12245
+ const sandbox = await createSandbox(options);
12246
+ try {
12247
+ return await sandbox.run(options);
12248
+ } finally {
12249
+ await sandbox.destroy();
12250
+ }
12353
12251
  }
12354
- async function down(args, dependencies) {
12355
- const context = await getContext(args, dependencies);
12356
- await destroyDeployment(context);
12357
- await destroyWorkspace(context, `${context.state.sandboxId}:workspace-destroy`).catch(() => {
12358
- return;
12252
+ async function resolvePrompt(options) {
12253
+ if (Boolean(options.prompt) === Boolean(options.promptFile))
12254
+ throw new Error("provide exactly one of prompt or promptFile");
12255
+ const promptFile = options.promptFile;
12256
+ let prompt = options.prompt ?? await readFile3(resolve(process.cwd(), promptFile ?? ""), "utf8");
12257
+ for (const [key, value] of Object.entries(options.promptArgs ?? {}))
12258
+ prompt = prompt.replaceAll(`{{${key}}}`, value);
12259
+ return prompt;
12260
+ }
12261
+ async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
12262
+ if (strategy.type === "head")
12263
+ return { hostCwd, runtimeCwd: hostCwd };
12264
+ const originalHead = await gitHead(hostCwd);
12265
+ if (strategy.type === "branch") {
12266
+ await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
12267
+ return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
12268
+ }
12269
+ const branch = strategy.name ?? `sandblocks/${randomUUID().slice(0, 8)}`;
12270
+ const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
12271
+ await mkdir(dirname(worktree), { recursive: true });
12272
+ await checked(["git", "-C", hostCwd, "worktree", "add", "-B", branch, worktree, "HEAD"], timeoutMs);
12273
+ return {
12274
+ hostCwd,
12275
+ runtimeCwd: worktree,
12276
+ branch,
12277
+ worktree,
12278
+ mergeToHead: strategy.type === "merge-to-head",
12279
+ originalHead
12280
+ };
12281
+ }
12282
+ async function mergeBranch(git, timeoutMs = 60000) {
12283
+ if (!git.branch)
12284
+ throw new Error("merge branch is unavailable");
12285
+ await checked(["git", "-C", git.hostCwd, "merge", "--no-edit", git.branch], timeoutMs);
12286
+ }
12287
+ async function gitHead(cwd) {
12288
+ const result = await executeProcess(["git", "-C", cwd, "rev-parse", "HEAD"]);
12289
+ return result.exitCode === 0 ? result.stdout.trim() : "";
12290
+ }
12291
+ async function collectCommits(cwd, baseline, timeoutMs = 30000) {
12292
+ if (!baseline)
12293
+ return [];
12294
+ const result = await executeProcess(["git", "-C", cwd, "log", "--format=%H%x00%s", `${baseline}..HEAD`], {
12295
+ timeoutMs
12359
12296
  });
12360
- await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`, { method: "DELETE" }).catch(() => {
12361
- return;
12297
+ if (result.exitCode !== 0 || !result.stdout.trim())
12298
+ return [];
12299
+ return result.stdout.trim().split(`
12300
+ `).map((line) => {
12301
+ const [sha, message] = line.split("\x00");
12302
+ if (!sha)
12303
+ throw new Error("Git returned an invalid commit record");
12304
+ return { sha, ...message ? { message } : {} };
12362
12305
  });
12363
- await rm3(context.file, { force: true });
12364
- console.log(`sandbox ${context.state.sandboxId} destroyed`);
12365
12306
  }
12366
- async function resolveAssignedTargetHost(context, fallback) {
12367
- if (fallback?.trim())
12368
- return fallback.trim();
12369
- const body = await request(context.apiUrl, context.apiKey, "/v1/hosts");
12370
- const host = Array.isArray(body.hosts) ? body.hosts.find((candidate) => candidate?.id === context.state.hostId) : undefined;
12371
- const targetHost = host?.labels?.targetHost;
12372
- return typeof targetHost === "string" && targetHost.trim() ? targetHost.trim() : undefined;
12373
- }
12374
- async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
12375
- const deploymentId = context.state.deploymentId;
12376
- if (!deploymentId)
12377
- throw new Error("deployment ID is required when recording sandbox routes");
12378
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
12379
- method: "POST",
12380
- body: JSON.stringify({
12381
- id: context.state.sandboxId,
12382
- goal: preview.goal,
12383
- status: status3,
12384
- stack: loaded.manifest.name ?? context.state.repository,
12385
- deploymentType: preview.deploymentType,
12386
- revisionId: deploymentId,
12387
- generation: Date.now(),
12388
- baseDomain: preview.baseDomain,
12389
- domains: await managedServiceDomains(context, loaded.manifest.name ?? context.state.repository, preview, deploymentId),
12390
- stableDomains: preview.stableDomains,
12391
- serviceRoutes,
12392
- healthEvidence,
12393
- images,
12394
- volumes: [
12395
- {
12396
- name: `${context.state.repository}-workspace`,
12397
- target: "/workspace",
12398
- access: "read-write"
12399
- }
12400
- ],
12401
- repositories: [context.state.repository],
12402
- executor: "sandblocks"
12403
- })
12404
- });
12405
- return body.sandbox;
12307
+ async function checked(command, timeoutMs) {
12308
+ const result = await executeProcess(command, { timeoutMs });
12309
+ if (result.exitCode !== 0)
12310
+ throw new Error(`${command[0]} failed: ${result.stderr || result.stdout}`);
12406
12311
  }
12407
- async function createEnvironmentSnapshot(context) {
12408
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots`, {
12409
- method: "POST",
12410
- body: JSON.stringify({
12411
- stack: context.state.repository,
12412
- app: "*",
12413
- environmentId: context.state.environment
12414
- })
12415
- });
12416
- const id2 = body.snapshot?.id;
12417
- if (typeof id2 !== "string" || !/^[a-f0-9-]{36}$/.test(id2)) {
12418
- throw new Error("managed environment snapshot response is invalid");
12312
+ async function runHostHooks(hooks, cwd) {
12313
+ for (const hook of hooks ?? []) {
12314
+ const command = hookCommand(hook);
12315
+ const result = await executeProcess(command, {
12316
+ cwd: resolve(cwd, hook.cwd ?? "."),
12317
+ env: hook.env,
12318
+ timeoutMs: hook.timeoutMs
12319
+ });
12320
+ if (result.exitCode !== 0)
12321
+ throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
12419
12322
  }
12420
- return id2;
12421
- }
12422
- async function releaseEnvironmentSnapshot(context, snapshotId) {
12423
- await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
12424
- }
12425
- async function destroyDeployment(context) {
12426
- if (!context.state.deploymentId)
12427
- return;
12428
- await destroyDeploymentById(context, context.state.deploymentId);
12429
- const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...remainingState } = context.state;
12430
- context.state = remainingState;
12431
- await writeState(context.file, context.state);
12432
12323
  }
12433
- async function destroyDeploymentById(context, deploymentId) {
12434
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12435
- method: "POST",
12436
- headers: { "idempotency-key": `preview:${deploymentId}:destroy:${Date.now()}` },
12437
- body: JSON.stringify({
12438
- kind: "workspace.service.destroy",
12439
- payload: { deploymentId, placement: { hostId: context.state.hostId } },
12440
- maxAttempts: 1
12441
- })
12442
- });
12443
- await wait(context.apiUrl, context.apiKey, body.operation.id);
12324
+ async function runSandboxHooks(runtime, hooks) {
12325
+ for (const hook of hooks ?? []) {
12326
+ const result = await runtime.exec(hookCommand(hook), {
12327
+ cwd: hook.cwd,
12328
+ env: hook.env,
12329
+ timeoutMs: hook.timeoutMs
12330
+ });
12331
+ if (result.exitCode !== 0)
12332
+ throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
12333
+ }
12444
12334
  }
12445
- async function destroyWorkspace(context, idempotencyKey) {
12446
- const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
12447
- method: "DELETE",
12448
- headers: {
12449
- "content-type": "application/json",
12450
- "x-sandblocks-api-key": context.apiKey,
12451
- "idempotency-key": idempotencyKey
12452
- },
12453
- body: JSON.stringify({ idempotencyKey })
12454
- });
12455
- const body = await response.json().catch(() => ({}));
12456
- if (response.ok)
12457
- await wait(context.apiUrl, context.apiKey, body.operation.id);
12458
- else if (![404, 409].includes(response.status))
12459
- throw new Error(String(body.error ?? "workspace destroy failed"));
12335
+ function hookCommand(hook) {
12336
+ if (Boolean(hook.command) === Boolean(hook.shell))
12337
+ throw new Error("hook requires exactly one of command or shell");
12338
+ if (hook.command)
12339
+ return hook.command;
12340
+ if (!hook.shell)
12341
+ throw new Error("hook shell command is required");
12342
+ return ["sh", "-lc", hook.shell];
12460
12343
  }
12461
- async function importSource(input) {
12462
- const source = await input.source(input.root);
12463
- const response = await fetchWithRetry(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
12464
- method: "POST",
12465
- headers: {
12466
- "content-type": "application/x-tar",
12467
- "content-length": String(source.bundle.byteLength),
12468
- "x-sandblocks-api-key": input.apiKey,
12469
- "x-sandblocks-workspace-id": input.workspaceId,
12470
- "x-sandblocks-sandbox-id": input.sandboxId,
12471
- "x-sandblocks-worker-pool": input.pool,
12472
- ...input.workspaceImage ? { "x-sandblocks-workspace-image": input.workspaceImage } : {},
12473
- "idempotency-key": input.idempotencyKey
12474
- },
12475
- body: new Blob([Uint8Array.from(source.bundle)])
12476
- });
12477
- const submitted = await response.json().catch(() => ({}));
12478
- if (!response.ok)
12479
- throw new Error(String(submitted?.error ?? `sandbox import failed (${response.status})`));
12480
- return {
12481
- files: source.files,
12482
- bytes: source.bundle.byteLength,
12483
- operation: await wait(input.apiUrl, input.apiKey, submitted.operation.id)
12344
+ function createLogger(options, cwd) {
12345
+ const config = options ?? { type: "file" };
12346
+ const file = resolve(cwd, config.path ?? `.sandblocks/logs/${Date.now()}.log`);
12347
+ return async (event) => {
12348
+ const line = `${JSON.stringify(event)}
12349
+ `;
12350
+ if (config.type === "stdout")
12351
+ process.stdout.write(event.type === "text" ? event.text : event.type === "raw" ? `${event.line}
12352
+ ` : line);
12353
+ if (config.type !== "stdout" && config.type !== "silent") {
12354
+ await mkdir(dirname(file), { recursive: true });
12355
+ await appendFile(file, line);
12356
+ }
12357
+ try {
12358
+ await config.onAgentStreamEvent?.(event);
12359
+ } catch {}
12484
12360
  };
12485
12361
  }
12486
- async function dataOperation(context, action, body) {
12487
- context.state.sequence += 1;
12488
- await writeState(context.file, context.state);
12489
- const submitted = await leaseRequest(context, `/v1/sandboxes/${context.state.sandboxId}/workspaces/${context.state.repository}/${action}`, {
12490
- method: "POST",
12491
- headers: { "idempotency-key": `cli:${context.state.sandboxId}:${context.state.sequence}:${action}` },
12492
- body: JSON.stringify(body)
12493
- });
12494
- return waitLease(context, submitted.operation.id);
12495
- }
12496
- async function getContext(args, dependencies) {
12497
- const options = dependencies.parse(args);
12498
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12499
- await loadLocalEnvironment(root);
12500
- const environment = dependencies.option(options, "environment") ?? "preview";
12501
- const file = statePath(root, environment);
12502
- let state;
12362
+ async function withTimeout(promise, timeoutMs, label) {
12363
+ let timer;
12503
12364
  try {
12504
- state = JSON.parse(await readFile4(file, "utf8"));
12505
- } catch {
12506
- throw new Error(`sandbox '${environment}' does not exist; use sandbox create or up`);
12365
+ return await Promise.race([
12366
+ promise,
12367
+ new Promise((_, reject) => {
12368
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
12369
+ })
12370
+ ]);
12371
+ } finally {
12372
+ if (timer)
12373
+ clearTimeout(timer);
12507
12374
  }
12508
- const configured = client(options, dependencies.option, state.projectId);
12509
- return { root, file, options, state, ...configured };
12510
12375
  }
12511
- function selectPreview(loaded, id2) {
12512
- return resolveEnvironment(loaded.manifest, id2);
12376
+ function assertProviderCapabilities(provider, options) {
12377
+ const capabilities = provider.capabilities;
12378
+ if (options.mounts?.length && !capabilities.bindMounts)
12379
+ throw new Error(`${provider.kind} provider does not support bind mounts`);
12380
+ if (options.network && !capabilities.networks)
12381
+ throw new Error(`${provider.kind} provider does not support network selection`);
12382
+ if (options.devices?.length && !capabilities.devices)
12383
+ throw new Error(`${provider.kind} provider does not support devices`);
12384
+ if ((options.retention === "always" || options.retention === "on-failure") && !capabilities.persistent)
12385
+ throw new Error(`${provider.kind} provider does not support retained sandboxes`);
12513
12386
  }
12514
- function client(options, option2, projectId) {
12515
- const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
12516
- const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY ?? "";
12517
- if (!apiUrl || !apiKey)
12518
- throw new Error("SANDBLOCKS_API_URL and SANDBLOCKS_API_KEY are required");
12519
- return { apiUrl, apiKey, projectId };
12387
+ function array(value) {
12388
+ return Array.isArray(value) ? value : [value];
12520
12389
  }
12521
- async function request(apiUrl, apiKey, pathname, init = {}) {
12522
- const headers = new Headers(init.headers);
12523
- headers.set("x-sandblocks-api-key", apiKey);
12524
- if (init.body)
12525
- headers.set("content-type", "application/json");
12526
- const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
12527
- const body = await response.json().catch(() => ({}));
12528
- if (!response.ok)
12529
- throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
12530
- return body;
12390
+ async function initSdk(options = {}) {
12391
+ const cwd = resolve2(options.cwd ?? process.cwd());
12392
+ const root = resolve2(cwd, ".sandblocks");
12393
+ const files = {
12394
+ "main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
12395
+ import { DockerProvider } from "@sandblocks/sdk/providers/docker";
12396
+
12397
+ await using sandbox = await createSandbox({
12398
+ provider: new DockerProvider({ image: "node:22-bookworm" }),
12399
+ cwd: process.cwd(),
12400
+ });
12401
+
12402
+ const result = await sandbox.run({
12403
+ agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
12404
+ promptFile: ".sandblocks/prompts/implement.md",
12405
+ logging: { type: "stdout" },
12406
+ });
12407
+
12408
+ console.log(result);
12409
+ `,
12410
+ "prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
12411
+
12412
+ <promise>COMPLETE</promise>
12413
+ `,
12414
+ ".env.example": `# Prefer Sandblocks managed secrets for remote providers.
12415
+ CLAUDE_CODE_OAUTH_TOKEN=
12416
+ # ANTHROPIC_API_KEY=
12417
+ SANDBLOCKS_API_URL=
12418
+ SANDBLOCKS_API_KEY=
12419
+ `,
12420
+ "config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
12421
+ `
12422
+ };
12423
+ const written = [];
12424
+ for (const [relative, content] of Object.entries(files)) {
12425
+ const path3 = resolve2(root, relative);
12426
+ await mkdir2(resolve2(path3, ".."), { recursive: true });
12427
+ if (!options.force && await access(path3).then(() => true).catch(() => false))
12428
+ continue;
12429
+ await writeFile(path3, content, { mode: relative.includes(".env") ? 384 : 420 });
12430
+ written.push(path3);
12431
+ }
12432
+ return written;
12531
12433
  }
12532
- async function leaseRequest(context, pathname, init = {}) {
12533
- const headers = new Headers(init.headers);
12534
- headers.set("authorization", `Bearer ${context.state.leaseToken}`);
12535
- if (init.body)
12536
- headers.set("content-type", "application/json");
12537
- const response = await fetchWithRetry(`${context.apiUrl}${pathname}`, { ...init, headers });
12538
- const body = await response.json().catch(() => ({}));
12539
- if (!response.ok)
12540
- throw new Error(String(body.error ?? `Sandblocks lease request failed (${response.status})`));
12541
- return body;
12542
- }
12543
- async function waitForPublishedRoutes(routes, timeoutMs = 90000) {
12544
- const deadline = Date.now() + timeoutMs;
12545
- let pending = routes.map((route) => ({
12546
- label: route.url,
12547
- url: route.service === "api" ? new URL("/health", route.url).toString() : route.url
12548
- }));
12549
- while (pending.length && Date.now() < deadline) {
12550
- const checks = await Promise.all(pending.map(async (route) => {
12551
- try {
12552
- const response = await fetch(route.url, { redirect: "manual" });
12553
- await response.body?.cancel().catch(() => {
12554
- return;
12555
- });
12556
- return response.status >= 200 && response.status < 400 ? undefined : route;
12557
- } catch {
12558
- return route;
12559
- }
12560
- }));
12561
- pending = checks.filter((route) => typeof route === "object" && route !== null);
12562
- if (pending.length)
12563
- await Bun.sleep(2000);
12434
+
12435
+ class SandblocksClient {
12436
+ options;
12437
+ baseUrl;
12438
+ fetchImpl;
12439
+ constructor(options) {
12440
+ this.options = options;
12441
+ if (!options.apiKey.trim())
12442
+ throw new Error("Sandblocks API key is required");
12443
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
12444
+ this.fetchImpl = options.fetchImpl ?? fetch;
12564
12445
  }
12565
- if (pending.length) {
12566
- throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
12446
+ listStacks(projectId) {
12447
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/stacks`);
12567
12448
  }
12568
- }
12569
- async function fetchWithRetry(url, init = {}) {
12570
- const method = (init.method ?? "GET").toUpperCase();
12571
- const headers = new Headers(init.headers);
12572
- const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
12573
- const attempts = safeToRetry ? 8 : 1;
12574
- let lastError;
12575
- for (let attempt = 0;attempt < attempts; attempt += 1) {
12576
- try {
12577
- const response = await fetch(url, init);
12578
- if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
12579
- return response;
12580
- await response.body?.cancel().catch(() => {
12581
- return;
12582
- });
12583
- } catch (error) {
12584
- lastError = error;
12585
- if (attempt === attempts - 1)
12586
- throw error;
12449
+ getStack(projectId, stack) {
12450
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}`);
12451
+ }
12452
+ listApps(projectId, stack) {
12453
+ const path3 = stack ? `/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}/apps` : `/v1/projects/${encodeURIComponent(projectId)}/apps`;
12454
+ return this.request(path3);
12455
+ }
12456
+ getApp(projectId, stack, app) {
12457
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}/apps/${encodeURIComponent(app)}`);
12458
+ }
12459
+ getResourceSettings(projectId, stack, app) {
12460
+ return this.request(this.resourcePath(projectId, stack, app, "settings"));
12461
+ }
12462
+ saveResourceSettings(input) {
12463
+ return this.request(this.resourcePath(input.projectId, input.stack, input.app, "settings"), {
12464
+ method: "PUT",
12465
+ body: JSON.stringify({ values: input.values, expectedRevision: input.expectedRevision })
12466
+ });
12467
+ }
12468
+ listDeploymentSecrets(projectId, stack, app) {
12469
+ return this.request(this.secretPath(projectId, stack, app));
12470
+ }
12471
+ saveDeploymentSecret(input) {
12472
+ return this.request(this.secretPath(input.projectId, input.stack, input.app), {
12473
+ method: "POST",
12474
+ body: JSON.stringify({ environment: input.environment, name: input.name, value: input.value })
12475
+ });
12476
+ }
12477
+ deleteDeploymentSecret(projectId, secretId, stack, app) {
12478
+ return this.request(`${this.secretPath(projectId, stack, app)}/${encodeURIComponent(secretId)}`, {
12479
+ method: "DELETE"
12480
+ });
12481
+ }
12482
+ listSandboxes(projectId) {
12483
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes`);
12484
+ }
12485
+ listAgentSessions(projectId, sandboxId) {
12486
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions`);
12487
+ }
12488
+ createAgentSession(input) {
12489
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/sandboxes/${encodeURIComponent(input.sandboxId)}/agent-sessions`, {
12490
+ method: "POST",
12491
+ headers: { "idempotency-key": input.idempotencyKey },
12492
+ body: JSON.stringify({
12493
+ repository: input.repository,
12494
+ provider: input.provider,
12495
+ model: input.model,
12496
+ prompt: input.prompt,
12497
+ credentialEnvironment: input.credentialEnvironment,
12498
+ timeoutSeconds: input.timeoutSeconds
12499
+ })
12500
+ });
12501
+ }
12502
+ getAgentSession(projectId, sandboxId, sessionId) {
12503
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions/${encodeURIComponent(sessionId)}`);
12504
+ }
12505
+ redeployAgentSession(projectId, sandboxId, sessionId, idempotencyKey) {
12506
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions/${encodeURIComponent(sessionId)}/redeploy`, { method: "POST", headers: { "idempotency-key": idempotencyKey } });
12507
+ }
12508
+ cancelAgentSession(projectId, sandboxId, sessionId) {
12509
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" });
12510
+ }
12511
+ listHosts(projectId) {
12512
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/hosts`);
12513
+ }
12514
+ recordSandbox(projectId, sandbox) {
12515
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes`, {
12516
+ method: "POST",
12517
+ body: JSON.stringify(sandbox)
12518
+ });
12519
+ }
12520
+ deployServices(input) {
12521
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/deployments`, {
12522
+ method: "POST",
12523
+ headers: { "idempotency-key": input.idempotencyKey },
12524
+ body: JSON.stringify(input)
12525
+ });
12526
+ }
12527
+ destroyServices(input) {
12528
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/deployments/${encodeURIComponent(input.deploymentId)}`, {
12529
+ method: "DELETE",
12530
+ headers: { "idempotency-key": input.idempotencyKey }
12531
+ });
12532
+ }
12533
+ issueSandboxLease(input) {
12534
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/sandboxes/${encodeURIComponent(input.sandboxId)}/leases`, {
12535
+ method: "POST",
12536
+ body: JSON.stringify({ subject: input.subject, scopes: input.scopes, ttlSeconds: input.ttlSeconds })
12537
+ });
12538
+ }
12539
+ listSandboxLeases(projectId, sandboxId) {
12540
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/leases`);
12541
+ }
12542
+ revokeSandboxLease(projectId, sandboxId, leaseId) {
12543
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/leases/${encodeURIComponent(leaseId)}`, { method: "DELETE" });
12544
+ }
12545
+ listRoutingAliases(projectId) {
12546
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases`);
12547
+ }
12548
+ listRoutingHistory(projectId) {
12549
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/history`);
12550
+ }
12551
+ promoteRoutingAliases(projectId, sandboxId) {
12552
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/promote`, {
12553
+ method: "POST",
12554
+ body: JSON.stringify({ sandboxId })
12555
+ });
12556
+ }
12557
+ getRoutingPolicy(projectId, stack, deploymentType) {
12558
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(deploymentType)}`);
12559
+ }
12560
+ setRoutingPolicy(projectId, stack, deploymentType, policy) {
12561
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(deploymentType)}`, {
12562
+ method: "PUT",
12563
+ body: JSON.stringify(policy)
12564
+ });
12565
+ }
12566
+ rollbackRoutingAliases(projectId, sandboxId) {
12567
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/rollback`, {
12568
+ method: "POST",
12569
+ body: JSON.stringify({ sandboxId })
12570
+ });
12571
+ }
12572
+ getSandbox(projectId, sandboxId) {
12573
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}`);
12574
+ }
12575
+ createWorkspace(input) {
12576
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces`, {
12577
+ method: "POST",
12578
+ headers: {
12579
+ "idempotency-key": input.idempotencyKey,
12580
+ ...input.gitCredential ? { "x-sandblocks-git-credential": input.gitCredential } : {}
12581
+ },
12582
+ body: JSON.stringify({
12583
+ workspaceId: input.workspaceId,
12584
+ repositoryUrl: input.repositoryUrl,
12585
+ revision: input.revision,
12586
+ placement: input.placement
12587
+ })
12588
+ });
12589
+ }
12590
+ executeWorkspace(input) {
12591
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(input.workspaceId)}/exec`, {
12592
+ method: "POST",
12593
+ headers: { "idempotency-key": input.idempotencyKey },
12594
+ body: JSON.stringify({ executable: input.executable, args: input.args })
12595
+ });
12596
+ }
12597
+ destroyWorkspace(input) {
12598
+ return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(input.workspaceId)}`, { method: "DELETE", headers: { "idempotency-key": input.idempotencyKey } });
12599
+ }
12600
+ async uploadPrebuiltArtifact(input) {
12601
+ if (!input.bundle.byteLength)
12602
+ throw new Error("Prebuilt artifact bundle cannot be empty");
12603
+ const headers = new Headers({
12604
+ "x-sandblocks-api-key": this.options.apiKey,
12605
+ "content-type": "application/x-tar",
12606
+ "content-length": String(input.bundle.byteLength),
12607
+ "x-sandblocks-workspace-id": input.workspaceId,
12608
+ "x-sandblocks-worker-pool": input.pool ?? "build-fleet",
12609
+ "idempotency-key": `prebuilt:${input.deploymentId}:import`
12610
+ });
12611
+ if (input.workspaceImage)
12612
+ headers.set("x-sandblocks-workspace-image", input.workspaceImage);
12613
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, { method: "POST", headers, body: new Blob([Uint8Array.from(input.bundle)]) });
12614
+ const imported = await response.json().catch(() => ({}));
12615
+ if (!response.ok || !imported.operation)
12616
+ throw new Error(imported.error ?? `Prebuilt artifact import failed (${response.status})`);
12617
+ await this.waitForOperation(imported.operation.id, { timeoutMs: input.timeoutMs ?? 30 * 60000 });
12618
+ const published = await this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/operations`, {
12619
+ method: "POST",
12620
+ headers: { "idempotency-key": `prebuilt:${input.deploymentId}:publish` },
12621
+ body: JSON.stringify({
12622
+ kind: "workspace.artifact.publish",
12623
+ payload: {
12624
+ workspaceId: input.workspaceId,
12625
+ deploymentId: input.deploymentId,
12626
+ stack: input.stack,
12627
+ placement: { requiredWorkerRole: "build", pool: input.pool ?? "build-fleet" }
12628
+ },
12629
+ maxAttempts: 3
12630
+ })
12631
+ });
12632
+ const operation = await this.waitForOperation(published.operation.id, {
12633
+ timeoutMs: input.timeoutMs ?? 30 * 60000
12634
+ });
12635
+ const image = String(operation.result?.image ?? "");
12636
+ if (!/@sha256:[a-f0-9]{64}$/.test(image))
12637
+ throw new Error("Published artifact digest is invalid");
12638
+ return { image, workspaceId: input.workspaceId, deploymentId: input.deploymentId, bytes: input.bundle.byteLength };
12639
+ }
12640
+ async deployPrebuiltArtifact(input) {
12641
+ const { operation } = await this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/operations`, {
12642
+ method: "POST",
12643
+ headers: { "idempotency-key": `prebuilt:${input.deploymentId}:deploy` },
12644
+ body: JSON.stringify({
12645
+ kind: "workspace.service.deploy",
12646
+ stack: input.stack,
12647
+ environmentId: input.environmentId,
12648
+ payload: {
12649
+ deploymentId: input.deploymentId,
12650
+ artifactImage: input.artifactImage,
12651
+ stack: input.stack,
12652
+ targetHost: input.targetHost,
12653
+ services: input.services,
12654
+ serviceUrls: input.serviceUrls ?? {},
12655
+ workspace: input.workspace ?? {},
12656
+ placement: input.placement ?? { requiredWorkerRole: "runtime" }
12657
+ },
12658
+ maxAttempts: 3
12659
+ })
12660
+ });
12661
+ return this.waitForOperation(operation.id, { timeoutMs: input.timeoutMs ?? 30 * 60000 });
12662
+ }
12663
+ getOperation(operationId) {
12664
+ return this.request(`/v1/operations/${encodeURIComponent(operationId)}`);
12665
+ }
12666
+ listOperations(projectId, sandboxId) {
12667
+ const query = sandboxId ? `?sandboxId=${encodeURIComponent(sandboxId)}` : "";
12668
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/operations${query}`);
12669
+ }
12670
+ listOperationEvents(operationId) {
12671
+ return this.request(`/v1/operations/${encodeURIComponent(operationId)}/events`);
12672
+ }
12673
+ cancelOperation(operationId) {
12674
+ return this.request(`/v1/operations/${encodeURIComponent(operationId)}/cancel`, { method: "POST" });
12675
+ }
12676
+ cancelSandboxOperations(projectId, sandboxId) {
12677
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/operations/cancel`, { method: "POST" });
12678
+ }
12679
+ async waitForOperation(operationId, options = {}) {
12680
+ const deadline = Date.now() + (options.timeoutMs ?? 120000);
12681
+ while (Date.now() < deadline) {
12682
+ const { operation } = await this.getOperation(operationId);
12683
+ if (operation.state === "succeeded")
12684
+ return operation;
12685
+ if (["failed", "cancelled"].includes(operation.state)) {
12686
+ throw new Error(operation.failureReason ?? `Sandblocks operation ${operation.state}`);
12687
+ }
12688
+ await new Promise((resolve3) => setTimeout(resolve3, options.pollMs ?? 500));
12587
12689
  }
12588
- await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
12690
+ throw new Error("Sandblocks operation timed out");
12691
+ }
12692
+ resourcePath(projectId, stack, app, suffix) {
12693
+ return `/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}${app ? `/apps/${encodeURIComponent(app)}` : ""}/${suffix}`;
12694
+ }
12695
+ secretPath(projectId, stack, app) {
12696
+ if (stack && app)
12697
+ return this.resourcePath(projectId, stack, app, "deployment-secrets");
12698
+ if (stack)
12699
+ return `/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}/deployment-secrets`;
12700
+ return `/v1/projects/${encodeURIComponent(projectId)}/deployment-secrets`;
12701
+ }
12702
+ async request(path3, init2 = {}) {
12703
+ const headers = new Headers(init2.headers);
12704
+ headers.set("x-sandblocks-api-key", this.options.apiKey);
12705
+ if (init2.body)
12706
+ headers.set("content-type", "application/json");
12707
+ const response = await this.fetchImpl(`${this.baseUrl}${path3}`, { ...init2, headers });
12708
+ const body = await response.json().catch(() => ({}));
12709
+ if (!response.ok)
12710
+ throw new Error(body.error ?? `Sandblocks request failed (${response.status})`);
12711
+ return body;
12589
12712
  }
12590
- throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
12591
12713
  }
12592
- async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
12593
- const deadline = Date.now() + timeoutMs;
12594
- while (Date.now() < deadline) {
12595
- const body = await request(apiUrl, apiKey, `/v1/operations/${operationId}`);
12596
- if (body.operation.state === "succeeded")
12597
- return body.operation;
12598
- if (["failed", "cancelled"].includes(body.operation.state))
12599
- throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
12600
- await Bun.sleep(500);
12714
+
12715
+ // packages/libs/cli/src/hooks.ts
12716
+ import { chmod, copyFile, mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
12717
+ import path3 from "path";
12718
+ var marker = "# sandblocks-managed-post-commit";
12719
+ var backupName = "post-commit.before-sandblocks";
12720
+ async function runHooksCommand(args, dependencies) {
12721
+ const [command = "status", ...rest] = args;
12722
+ if (command === "run") {
12723
+ const [hook, directory] = rest;
12724
+ if (hook !== "post-commit")
12725
+ throw new Error("hooks run supports only post-commit");
12726
+ return runPostCommit(path3.resolve(directory ?? process.cwd()), dependencies);
12601
12727
  }
12602
- throw new Error(`operation ${operationId} timed out`);
12728
+ const root = path3.resolve(rest.find((value) => !value.startsWith("--")) ?? process.cwd());
12729
+ if (command === "install")
12730
+ return install(root);
12731
+ if (command === "uninstall")
12732
+ return uninstall(root);
12733
+ if (command === "status")
12734
+ return status(root);
12735
+ throw new Error(`unsupported hooks command '${command}'`);
12603
12736
  }
12604
- async function waitLease(context, operationId) {
12605
- const deadline = Date.now() + 3 * 60000;
12606
- while (Date.now() < deadline) {
12607
- const body = await leaseRequest(context, `/v1/sandbox-operations/${operationId}`);
12608
- if (body.operation.state === "succeeded")
12609
- return body.operation;
12610
- if (["failed", "cancelled"].includes(body.operation.state))
12611
- throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
12612
- await Bun.sleep(500);
12737
+ async function install(root) {
12738
+ const hooksDir = await gitPath(root, "hooks");
12739
+ const hookPath = path3.join(hooksDir, "post-commit");
12740
+ const backupPath = path3.join(hooksDir, backupName);
12741
+ await mkdir3(hooksDir, { recursive: true });
12742
+ const current = await text(hookPath);
12743
+ if (current && !current.includes(marker)) {
12744
+ await copyFile(hookPath, backupPath);
12745
+ await chmod(backupPath, 493);
12613
12746
  }
12614
- throw new Error(`sandbox operation ${operationId} timed out`);
12747
+ await writeFile2(hookPath, `#!/bin/sh
12748
+ ${marker}
12749
+ set -u
12750
+ previous="$(dirname "$0")/${backupName}"
12751
+ if [ -x "$previous" ]; then "$previous" || exit $?; fi
12752
+ root=$(git rev-parse --show-toplevel) || exit $?
12753
+ cd "$root" || exit $?
12754
+ if [ -x node_modules/.bin/sandblocks ]; then exec node_modules/.bin/sandblocks hooks run post-commit "$root"; fi
12755
+ exec sandblocks hooks run post-commit "$root"
12756
+ `);
12757
+ await chmod(hookPath, 493);
12758
+ console.log(`installed ${hookPath}`);
12759
+ if (await exists(backupPath))
12760
+ console.log(`chaining ${backupPath}`);
12615
12761
  }
12616
- async function loadLocalEnvironment(root) {
12617
- const file = path5.join(root, ".sandblocks", "config.env");
12618
- let contents;
12619
- try {
12620
- contents = await readFile4(file, "utf8");
12621
- } catch {
12622
- return;
12762
+ async function uninstall(root) {
12763
+ const hooksDir = await gitPath(root, "hooks");
12764
+ const hookPath = path3.join(hooksDir, "post-commit");
12765
+ const backupPath = path3.join(hooksDir, backupName);
12766
+ const current = await text(hookPath);
12767
+ if (!current?.includes(marker))
12768
+ throw new Error("Sandblocks post-commit hook is not installed");
12769
+ if (await exists(backupPath)) {
12770
+ await copyFile(backupPath, hookPath);
12771
+ await chmod(hookPath, 493);
12772
+ await rm2(backupPath);
12773
+ console.log(`restored ${hookPath}`);
12774
+ } else {
12775
+ await rm2(hookPath);
12776
+ console.log(`removed ${hookPath}`);
12623
12777
  }
12624
- for (const raw of contents.split(/\r?\n/)) {
12625
- const line = raw.trim();
12626
- if (!line || line.startsWith("#") || !line.includes("="))
12627
- continue;
12628
- const [key, ...parts] = line.split("=");
12629
- if (!key?.startsWith("SANDBLOCKS_") || process.env[key] !== undefined)
12630
- continue;
12631
- process.env[key] = parts.join("=").trim().replace(/^['"]|['"]$/g, "");
12778
+ }
12779
+ async function status(root) {
12780
+ const hooksDir = await gitPath(root, "hooks");
12781
+ const installed = (await text(path3.join(hooksDir, "post-commit")))?.includes(marker) === true;
12782
+ const loaded = await loadPolicy(root);
12783
+ console.log(`post-commit ${installed ? "installed" : "not installed"}`);
12784
+ console.log(`policy ${loaded ? loaded.enabled ? "enabled" : "disabled" : "not configured"}`);
12785
+ if (loaded) {
12786
+ console.log(`branches ${loaded.branches.join(",")}`);
12787
+ console.log(`environment ${loaded.environment}`);
12788
+ console.log(`mode ${loaded.background ? "background" : "foreground"}`);
12632
12789
  }
12633
12790
  }
12634
- async function applyManagedRoutingPolicy(context, stack, preview) {
12635
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
12636
- const policy = body.policy;
12637
- const primaryDomains = Object.fromEntries(Object.entries(policy.domains ?? {}).flatMap(([service, domains]) => {
12638
- const domain = Array.isArray(domains) ? domains[0] : domains;
12639
- return domain ? [[service, domain]] : [];
12640
- }));
12641
- return {
12642
- ...preview,
12643
- baseDomain: policy.baseDomain ?? preview.baseDomain,
12644
- domains: { ...preview.domains, ...primaryDomains },
12645
- stableDomains: { ...preview.stableDomains, ...policy.stableDomains }
12646
- };
12791
+ async function runPostCommit(root, dependencies) {
12792
+ if (process.env.SANDBLOCKS_SKIP_POST_COMMIT === "1") {
12793
+ console.log("sandblocks post-commit skipped by SANDBLOCKS_SKIP_POST_COMMIT");
12794
+ return;
12795
+ }
12796
+ const policy = (await dependencies.load(root)).manifest.hooks.postCommit;
12797
+ if (!policy?.enabled)
12798
+ return;
12799
+ const branch = await git(root, ["branch", "--show-current"]);
12800
+ if (!policy.branches.includes(branch)) {
12801
+ console.log(`sandblocks post-commit skipped on branch ${branch || "(detached)"}`);
12802
+ return;
12803
+ }
12804
+ const stateFile = path3.join(root, ".sandblocks", `sandbox-${policy.environment}.json`);
12805
+ const command = await exists(stateFile) ? "redeploy" : "up";
12806
+ const commandArgs = [root, "--environment", policy.environment];
12807
+ if (policy.background) {
12808
+ const logDir = path3.join(root, ".sandblocks", "logs");
12809
+ await mkdir3(logDir, { recursive: true });
12810
+ const log = path3.join(logDir, `post-commit-${Date.now()}.log`);
12811
+ const executable = process.argv[1];
12812
+ if (!executable)
12813
+ throw new Error("unable to resolve Sandblocks CLI executable");
12814
+ const proc = Bun.spawn([process.execPath, executable, "sandbox", command, ...commandArgs], {
12815
+ cwd: root,
12816
+ stdout: Bun.file(log),
12817
+ stderr: Bun.file(log),
12818
+ env: { ...process.env, SANDBLOCKS_SKIP_POST_COMMIT: "1" }
12819
+ });
12820
+ proc.unref();
12821
+ console.log(`sandblocks ${command} started for ${policy.environment} (${log})`);
12822
+ return;
12823
+ }
12824
+ await dependencies.runSandbox(command, commandArgs);
12647
12825
  }
12648
- function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
12649
- return Object.fromEntries(preview.services.flatMap((service) => {
12650
- if (service.kind === "process" && !service.port)
12651
- return [];
12652
- const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
12653
- const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
12654
- const configuredStableHosts = preview.stableDomains[service.id];
12655
- const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.deploymentType}-${stack}-${service.id}.${preview.baseDomain}`;
12656
- return [
12657
- [`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
12658
- [`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
12659
- ];
12660
- }));
12826
+ async function loadPolicy(root) {
12827
+ const { loadSandblocksManifest: loadSandblocksManifest2 } = await Promise.resolve().then(() => (init_src(), exports_src));
12828
+ return (await loadSandblocksManifest2({ repositoryRoot: root })).manifest.hooks.postCommit;
12661
12829
  }
12662
- async function managedServiceDomains(context, stack, preview, deploymentId) {
12663
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
12664
- const domains = body.policy.domains ?? {};
12665
- return Object.keys(domains).length ? domains : resolvedServiceDomains(context.state.sandboxId, deploymentId, preview);
12830
+ async function gitPath(root, name) {
12831
+ return path3.resolve(root, await git(root, ["rev-parse", "--git-path", name]));
12666
12832
  }
12667
- function resolvedServiceDomains(sandboxId, deploymentId, preview) {
12668
- return Object.fromEntries(preview.services.map((service) => [
12669
- service.id,
12670
- resolvedServiceDomain(sandboxId, deploymentId, service.id, preview)
12671
- ]));
12833
+ async function git(root, args) {
12834
+ const proc = Bun.spawn(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
12835
+ const [stdout, stderr, code] = await Promise.all([
12836
+ new Response(proc.stdout).text(),
12837
+ new Response(proc.stderr).text(),
12838
+ proc.exited
12839
+ ]);
12840
+ if (code !== 0)
12841
+ throw new Error(stderr.trim() || `git ${args.join(" ")} failed`);
12842
+ return stdout.trim();
12672
12843
  }
12673
- function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
12674
- const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${deploymentId}/${service}`).digest("hex").slice(0, 8)}`;
12675
- const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
12676
- return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
12844
+ async function exists(file) {
12845
+ return Bun.file(file).exists();
12677
12846
  }
12678
- function statePath(root, environment) {
12679
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(environment))
12680
- throw new Error("sandbox environment is invalid");
12681
- return path5.join(root, ".sandblocks", `sandbox-${environment}.json`);
12847
+ async function text(file) {
12848
+ try {
12849
+ return await readFile4(file, "utf8");
12850
+ } catch {
12851
+ return;
12852
+ }
12682
12853
  }
12683
- async function writeState(file, state) {
12684
- await mkdir3(path5.dirname(file), { recursive: true, mode: 448 });
12685
- await chmod3(path5.dirname(file), 448);
12686
- const temporary = `${file}.${process.pid}.tmp`;
12687
- await Bun.write(temporary, `${JSON.stringify(state, null, 2)}
12688
- `, { mode: 384 });
12689
- await chmod3(temporary, 384);
12690
- await rename(temporary, file);
12854
+
12855
+ // packages/libs/cli/src/runtime.ts
12856
+ import { spawn as spawn2 } from "child_process";
12857
+ import { access as access2, chmod as chmod2, mkdir as mkdir5, readdir, rm as rm3 } from "fs/promises";
12858
+ import path4 from "path";
12859
+ var RUNTIME_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
12860
+ var NODE_ENV_RUNTIME = {
12861
+ production: "prod",
12862
+ development: "dev",
12863
+ test: "local"
12864
+ };
12865
+ async function runRuntimeCommand(args) {
12866
+ const [action, ...rest] = args;
12867
+ if (action !== "prepare")
12868
+ throw new Error("runtime action must be prepare");
12869
+ const options = parse(rest);
12870
+ const root = path4.resolve(options.positionals[0] ?? process.cwd());
12871
+ const runtime = normalizedRuntime(option(options, "runtime") ?? process.env.SANDBLOCKS_RUNTIME ?? "development" ?? "development");
12872
+ const targetRoot = contained(root, option(options, "target") ?? ".locker", "runtime target");
12873
+ const runtimeRoot = path4.join(targetRoot, runtime);
12874
+ const config = contained(root, option(options, "config") ?? "config/dotlocker.config.ts", "Dotlocker config");
12875
+ const dotlocker = path4.resolve(root, option(options, "dotlocker") ?? path4.join("node_modules", ".bin", "dotlocker"));
12876
+ if (!await exists2(config))
12877
+ throw new Error(`Dotlocker config does not exist: ${config}`);
12878
+ if (!await exists2(dotlocker)) {
12879
+ throw new Error("Dotlocker is not installed; add @dotlocker/dotlocker to the workspace");
12880
+ }
12881
+ await rm3(targetRoot, { recursive: true, force: true });
12882
+ await mkdir5(runtimeRoot, { recursive: true, mode: 493 });
12883
+ await run2(dotlocker, ["pull", "--config", config, "--runtime", runtime, "--out", runtimeRoot], root);
12884
+ const envFile = path4.join(runtimeRoot, ".env");
12885
+ if (!await exists2(envFile)) {
12886
+ throw new Error(`Dotlocker did not materialize ${path4.relative(root, envFile)}`);
12887
+ }
12888
+ await normalizePermissions(targetRoot);
12889
+ console.log(`runtime ${runtime}
12890
+ environment ${path4.relative(root, envFile)}`);
12691
12891
  }
12692
- function redactedState(state) {
12693
- return { ...state, leaseToken: "<redacted>" };
12892
+ function run2(executable, args, cwd) {
12893
+ return new Promise((resolve3, reject) => {
12894
+ const child = spawn2(executable, args, { cwd, env: process.env, stdio: "inherit" });
12895
+ child.once("error", reject);
12896
+ child.once("exit", (code, signal) => {
12897
+ if (code === 0)
12898
+ resolve3();
12899
+ else
12900
+ reject(new Error(`Dotlocker pull failed (${signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`})`));
12901
+ });
12902
+ });
12694
12903
  }
12695
- function printOutput(operation) {
12696
- if (operation.result?.stdout)
12697
- process.stdout.write(String(operation.result.stdout));
12698
- if (operation.result?.stderr)
12699
- process.stderr.write(String(operation.result.stderr));
12700
- if (Number(operation.result?.exitCode ?? 0) !== 0)
12701
- throw new Error(`command exited ${operation.result.exitCode}`);
12904
+ async function exists2(file) {
12905
+ try {
12906
+ await access2(file);
12907
+ return true;
12908
+ } catch {
12909
+ return false;
12910
+ }
12702
12911
  }
12703
-
12704
- // src/sdk.ts
12705
- import { mkdir as mkdir5, readFile as readFile7, rm as rm6, writeFile as writeFile4 } from "fs/promises";
12706
- import { dirname as dirname2, resolve as resolve7 } from "path";
12707
-
12708
- // ../sdk/dist/index.js
12709
- import { randomUUID as randomUUID2 } from "crypto";
12710
- import { appendFile, cp, mkdir as mkdir4, readFile as readFile5, rm as rm4 } from "fs/promises";
12711
- import { dirname, resolve } from "path";
12712
- import { spawn } from "child_process";
12713
- import { access, mkdir as mkdir22, writeFile as writeFile2 } from "fs/promises";
12714
- import { resolve as resolve2 } from "path";
12715
-
12716
- class CommandAgent {
12717
- options;
12718
- id;
12719
- constructor(options) {
12720
- this.options = options;
12721
- this.id = options.id ?? "command";
12912
+ function normalizedRuntime(value) {
12913
+ const runtime = NODE_ENV_RUNTIME[value.trim()] ?? value.trim();
12914
+ if (!RUNTIME_PATTERN.test(runtime))
12915
+ throw new Error(`invalid runtime '${value}'`);
12916
+ return runtime;
12917
+ }
12918
+ function contained(root, value, label) {
12919
+ const resolved = path4.resolve(root, value);
12920
+ const relative = path4.relative(root, resolved);
12921
+ if (!relative || !relative.startsWith("..") && !path4.isAbsolute(relative))
12922
+ return resolved;
12923
+ throw new Error(`${label} must remain within the repository`);
12924
+ }
12925
+ async function normalizePermissions(directory) {
12926
+ await chmod2(directory, 493);
12927
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
12928
+ const child = path4.join(directory, entry.name);
12929
+ if (entry.isSymbolicLink())
12930
+ throw new Error("Dotlocker output must not contain symbolic links");
12931
+ if (entry.isDirectory())
12932
+ await normalizePermissions(child);
12933
+ else if (entry.isFile())
12934
+ await chmod2(child, 420);
12935
+ else
12936
+ throw new Error("Dotlocker output contains an unsupported file type");
12722
12937
  }
12723
- async run(context) {
12724
- const startedAt = new Date().toISOString();
12725
- const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
12726
- const promptMode = this.options.prompt ?? "stdin";
12727
- const command = promptMode === "argument" ? [...base, context.prompt] : base;
12728
- const idle = new AbortController;
12729
- const completed = new AbortController;
12730
- let idleTimer;
12731
- let completionTimer;
12732
- const resetIdle = () => {
12733
- if (idleTimer)
12734
- clearTimeout(idleTimer);
12735
- idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
12736
- };
12737
- resetIdle();
12738
- let output = "";
12739
- let stdout = "";
12740
- let stderr = "";
12741
- let lineBuffer = "";
12742
- let completionSignal;
12743
- const consume = async (chunk, stream) => {
12744
- resetIdle();
12745
- output += chunk;
12746
- if (stream === "stdout")
12747
- stdout += chunk;
12748
- else
12749
- stderr += chunk;
12750
- lineBuffer += chunk;
12751
- const lines = lineBuffer.split(/\r?\n/);
12752
- lineBuffer = lines.pop() ?? "";
12753
- for (const line of lines)
12754
- await this.emitLine(line, stream, context);
12755
- completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
12756
- if (completionSignal && !completionTimer) {
12757
- completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
12758
- }
12759
- };
12760
- try {
12761
- const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
12762
- try {
12763
- const result = await context.runtime.exec(command, {
12764
- env: this.options.env,
12765
- stdin: promptMode === "stdin" ? context.prompt : undefined,
12766
- signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
12767
- onStdout: (chunk) => consume(chunk, "stdout"),
12768
- onStderr: (chunk) => consume(chunk, "stderr")
12769
- });
12770
- stdout = result.stdout;
12771
- stderr = result.stderr;
12772
- return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
12773
- } catch (error) {
12774
- if (!completionSignal)
12775
- throw error;
12776
- return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
12938
+ }
12939
+ function parse(args) {
12940
+ const options = { positionals: [] };
12941
+ for (let index = 0;index < args.length; index += 1) {
12942
+ const value = args[index] ?? "";
12943
+ if (!value.startsWith("--")) {
12944
+ options.positionals.push(value);
12945
+ continue;
12946
+ }
12947
+ const key = value.slice(2);
12948
+ const next = args[index + 1];
12949
+ if (next && !next.startsWith("--")) {
12950
+ options[key] = next;
12951
+ index += 1;
12952
+ } else
12953
+ options[key] = true;
12954
+ }
12955
+ return options;
12956
+ }
12957
+ function option(options, key) {
12958
+ const value = options[key];
12959
+ return typeof value === "string" ? value : undefined;
12960
+ }
12961
+
12962
+ // packages/libs/cli/src/sandbox.ts
12963
+ init_src();
12964
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
12965
+ import { chmod as chmod3, mkdir as mkdir6, readFile as readFile5, rename, rm as rm4 } from "fs/promises";
12966
+ import path5 from "path";
12967
+ var COMMANDS = new Set([
12968
+ "up",
12969
+ "create",
12970
+ "status",
12971
+ "deploy",
12972
+ "redeploy",
12973
+ "renew",
12974
+ "exec",
12975
+ "git",
12976
+ "down",
12977
+ "destroy"
12978
+ ]);
12979
+ function isStatefulSandboxCommand(command) {
12980
+ return Boolean(command && COMMANDS.has(command));
12981
+ }
12982
+ async function runStatefulSandboxCommand(command, args, dependencies) {
12983
+ if (command === "up")
12984
+ return up(args, dependencies);
12985
+ if (command === "create")
12986
+ return create(args, dependencies);
12987
+ if (command === "status")
12988
+ return status2(args, dependencies);
12989
+ if (command === "deploy")
12990
+ return deploy(args, dependencies);
12991
+ if (command === "redeploy")
12992
+ return redeploy(args, dependencies);
12993
+ if (command === "renew")
12994
+ return renew(args, dependencies);
12995
+ if (command === "exec")
12996
+ return exec(args, dependencies);
12997
+ if (command === "git")
12998
+ return git2(args, dependencies);
12999
+ return down(args, dependencies);
13000
+ }
13001
+ async function up(args, dependencies) {
13002
+ await withDeploymentLock(args, dependencies, async () => {
13003
+ await create(args, dependencies);
13004
+ try {
13005
+ await deployUnlocked(args, dependencies);
13006
+ } catch (error) {
13007
+ if (process.env.SANDBLOCKS_RETAIN_FAILED !== "true") {
13008
+ await down(args, dependencies).catch(() => {
13009
+ return;
13010
+ });
13011
+ }
13012
+ throw error;
13013
+ }
13014
+ });
13015
+ }
13016
+ async function create(args, dependencies) {
13017
+ const options = dependencies.parse(args);
13018
+ const root = path5.resolve(options.positionals[0] ?? process.cwd());
13019
+ await loadLocalEnvironment(root);
13020
+ const environment = dependencies.option(options, "environment") ?? "preview";
13021
+ const file = statePath(root, environment);
13022
+ if (await Bun.file(file).exists())
13023
+ throw new Error(`sandbox '${environment}' already exists`);
13024
+ const loaded = await dependencies.load(root, dependencies.option(options, "config") ?? dependencies.option(options, "manifest"));
13025
+ const preview = selectPreview(loaded, environment);
13026
+ const projectId = dependencies.option(options, "project") ?? process.env.SANDBLOCKS_PROJECT_ID ?? "default";
13027
+ const repository = loaded.manifest.name ?? path5.basename(root).toLowerCase();
13028
+ const sandboxId = randomUUID2();
13029
+ const workspaceId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${repository}`.slice(0, 63);
13030
+ const { apiUrl, apiKey } = client(options, dependencies.option, projectId);
13031
+ await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes`, {
13032
+ method: "POST",
13033
+ body: JSON.stringify({ id: sandboxId, goal: preview.goal, status: "active", repositories: [repository] })
13034
+ });
13035
+ try {
13036
+ const lease = await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}/leases`, {
13037
+ method: "POST",
13038
+ body: JSON.stringify({
13039
+ subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
13040
+ scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
13041
+ ttlSeconds: Number(dependencies.option(options, "ttl-seconds") ?? 28800)
13042
+ })
13043
+ });
13044
+ const imported = await importSource({
13045
+ root,
13046
+ projectId,
13047
+ sandboxId,
13048
+ workspaceId,
13049
+ pool: dependencies.option(options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "build-fleet",
13050
+ apiUrl,
13051
+ apiKey,
13052
+ idempotencyKey: `${sandboxId}:workspace-import`,
13053
+ source: dependencies.source,
13054
+ ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
13055
+ });
13056
+ const hostId = String(imported.operation.assignedHostId ?? "");
13057
+ if (!hostId)
13058
+ throw new Error("workspace import did not bind a sandbox host");
13059
+ const state = {
13060
+ version: 1,
13061
+ environment,
13062
+ projectId,
13063
+ sandboxId,
13064
+ workspaceId,
13065
+ repository,
13066
+ leaseId: lease.lease.id,
13067
+ leaseToken: lease.token,
13068
+ hostId,
13069
+ sequence: 0,
13070
+ createdAt: new Date().toISOString()
13071
+ };
13072
+ await writeState(file, state);
13073
+ console.log(`sandbox ${sandboxId}`);
13074
+ console.log(`workspace ${workspaceId}`);
13075
+ console.log(`host ${hostId}`);
13076
+ } catch (error) {
13077
+ await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
13078
+ method: "DELETE"
13079
+ }).catch(() => {
13080
+ return;
13081
+ });
13082
+ throw error;
13083
+ }
13084
+ }
13085
+ async function status2(args, dependencies) {
13086
+ const context = await getContext(args, dependencies);
13087
+ const [sandbox, session] = await Promise.all([
13088
+ request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`),
13089
+ leaseRequest(context, "/v1/sandbox-session")
13090
+ ]);
13091
+ if (context.options.json) {
13092
+ console.log(JSON.stringify({ state: redactedState(context.state), sandbox: sandbox.sandbox, session: session.session }, null, 2));
13093
+ return;
13094
+ }
13095
+ console.log(`state ${path5.relative(context.root, context.file)}`);
13096
+ console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
13097
+ console.log(`workspace ${context.state.workspaceId}`);
13098
+ console.log(`host ${context.state.hostId}`);
13099
+ console.log(`lease expires ${session.session.expiresAt}`);
13100
+ for (const route of context.state.previewUrls ?? [])
13101
+ console.log(`${route.service ?? "service"} ${route.url}`);
13102
+ }
13103
+ async function deploy(args, dependencies) {
13104
+ await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
13105
+ }
13106
+ async function withDeploymentLock(args, dependencies, action) {
13107
+ const options = dependencies.parse(args);
13108
+ const root = path5.resolve(options.positionals[0] ?? process.cwd());
13109
+ const environment = dependencies.option(options, "environment") ?? "preview";
13110
+ const lock = `${statePath(root, environment)}.deploy.lock`;
13111
+ await mkdir6(path5.dirname(lock), { recursive: true, mode: 448 });
13112
+ try {
13113
+ await mkdir6(lock);
13114
+ } catch (error) {
13115
+ if (error.code === "EEXIST") {
13116
+ throw new Error(`sandbox '${environment}' deployment is already running`);
13117
+ }
13118
+ throw error;
13119
+ }
13120
+ try {
13121
+ await action();
13122
+ } finally {
13123
+ await rm4(lock, { recursive: true, force: true });
13124
+ }
13125
+ }
13126
+ async function deployUnlocked(args, dependencies) {
13127
+ const context = await getContext(args, dependencies);
13128
+ if (context.state.deploymentId)
13129
+ throw new Error("preview is already deployed; use sandbox redeploy");
13130
+ const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
13131
+ const stack = loaded.manifest.name ?? context.state.repository;
13132
+ const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
13133
+ const deploymentTarget = await resolveDeploymentTarget(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
13134
+ const targetHost = deploymentTarget.targetHost;
13135
+ context.state.deploymentHostId = deploymentTarget.hostId;
13136
+ const resume = context.options.resume === true;
13137
+ const previousCandidate = resume ? context.state.candidate : undefined;
13138
+ if (resume && !previousCandidate)
13139
+ throw new Error("no resumable candidate exists for this sandbox");
13140
+ const requestedDeploymentId = dependencies.option(context.options, "deployment-id");
13141
+ const deploymentId = previousCandidate?.deploymentId ?? requestedDeploymentId ?? randomUUID2();
13142
+ if (!/^[a-f0-9-]{36}$/.test(deploymentId))
13143
+ throw new Error("deployment ID must be a UUID");
13144
+ if (previousCandidate && requestedDeploymentId && requestedDeploymentId !== previousCandidate.deploymentId)
13145
+ 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 };
13148
+ await writeState(context.file, context.state);
13149
+ const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
13150
+ const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
13151
+ let buildWorkspaceId = previousCandidate?.buildWorkspaceId;
13152
+ let buildHostId = previousCandidate?.buildHostId;
13153
+ const suppliedArtifact = dependencies.option(context.options, "artifact");
13154
+ if (suppliedArtifact && !/@sha256:[a-f0-9]{64}$/.test(suppliedArtifact))
13155
+ throw new Error("--artifact must be an immutable OCI image digest");
13156
+ let artifactImage = suppliedArtifact ?? previousCandidate?.artifactImage;
13157
+ if (suppliedArtifact) {
13158
+ context.state.candidate = { deploymentId, environmentSnapshotId, artifactImage: suppliedArtifact };
13159
+ await writeState(context.file, context.state);
13160
+ }
13161
+ if (!artifactImage && serviceSteps.length) {
13162
+ buildWorkspaceId ??= `${context.state.sandboxId.slice(0, 8)}-${context.state.repository}-build-${deploymentId.slice(0, 8)}`.slice(0, 63);
13163
+ try {
13164
+ if (!previousCandidate?.buildCompleted) {
13165
+ const imported = await importSource({
13166
+ root: context.root,
13167
+ projectId: context.state.projectId,
13168
+ workspaceId: buildWorkspaceId,
13169
+ pool: dependencies.option(context.options, "build-pool") ?? process.env.SANDBLOCKS_BUILD_WORKER_POOL ?? "build-fleet",
13170
+ apiUrl: context.apiUrl,
13171
+ apiKey: context.apiKey,
13172
+ idempotencyKey: `preview:${deploymentId}:build-import`,
13173
+ source: dependencies.source,
13174
+ ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
13175
+ });
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 };
13180
+ await writeState(context.file, context.state);
13181
+ const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13182
+ method: "POST",
13183
+ headers: { "idempotency-key": `preview:${deploymentId}:steps` },
13184
+ body: JSON.stringify({
13185
+ kind: "workspace.step.run",
13186
+ stack: context.state.repository,
13187
+ environmentId: context.state.environment,
13188
+ environmentSnapshotId,
13189
+ payload: {
13190
+ workspaceId: buildWorkspaceId,
13191
+ sandboxId: context.state.sandboxId,
13192
+ revisionId: deploymentId,
13193
+ runtimeType: preview.deploymentType,
13194
+ serviceUrls,
13195
+ steps: serviceSteps,
13196
+ ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
13197
+ placement: { hostId: buildHostId, memoryReservationBytes: 2 * 1024 * 1024 * 1024 }
13198
+ },
13199
+ maxAttempts: 1
13200
+ })
13201
+ });
13202
+ await wait(context.apiUrl, context.apiKey, stepOperation.operation.id, 60 * 60000);
13203
+ context.state.candidate = {
13204
+ deploymentId,
13205
+ environmentSnapshotId,
13206
+ buildWorkspaceId,
13207
+ buildHostId,
13208
+ buildCompleted: true
13209
+ };
13210
+ await writeState(context.file, context.state);
13211
+ }
13212
+ if (!buildHostId)
13213
+ throw new Error("resumable build host is unavailable");
13214
+ const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13215
+ method: "POST",
13216
+ headers: { "idempotency-key": `preview:${deploymentId}:artifact` },
13217
+ body: JSON.stringify({
13218
+ kind: "workspace.artifact.publish",
13219
+ payload: {
13220
+ workspaceId: buildWorkspaceId,
13221
+ deploymentId,
13222
+ stack,
13223
+ placement: { hostId: buildHostId, memoryReservationBytes: 1024 * 1024 * 1024 }
13224
+ },
13225
+ maxAttempts: 1
13226
+ })
13227
+ });
13228
+ const published = await wait(context.apiUrl, context.apiKey, publish.operation.id, 60 * 60000);
13229
+ artifactImage = String(published.result?.image ?? "");
13230
+ if (!/@sha256:[a-f0-9]{64}$/.test(artifactImage))
13231
+ throw new Error("build artifact digest is invalid");
13232
+ context.state.candidate = {
13233
+ deploymentId,
13234
+ environmentSnapshotId,
13235
+ buildWorkspaceId,
13236
+ buildHostId,
13237
+ buildCompleted: true,
13238
+ artifactImage
13239
+ };
13240
+ await writeState(context.file, context.state);
13241
+ } catch (error) {
13242
+ if (!context.state.candidate?.buildCompleted && buildWorkspaceId)
13243
+ await destroyDetachedWorkspace(context, buildWorkspaceId, `preview:${deploymentId}:build-cleanup`).catch(() => {
13244
+ return;
13245
+ });
13246
+ if (!context.state.candidate?.buildCompleted) {
13247
+ context.state.candidate = undefined;
13248
+ await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
13249
+ return;
13250
+ });
13251
+ await writeState(context.file, context.state);
13252
+ }
13253
+ const message = error instanceof Error ? error.message : String(error);
13254
+ throw new Error(`${message}${context.state.candidate?.buildCompleted ? "; retry with `sandblocks sandbox deploy . --resume`" : ""}`);
13255
+ }
13256
+ }
13257
+ if (!artifactImage) {
13258
+ const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13259
+ method: "POST",
13260
+ headers: { "idempotency-key": `preview:${deploymentId}:artifact` },
13261
+ body: JSON.stringify({
13262
+ kind: "workspace.artifact.publish",
13263
+ payload: {
13264
+ workspaceId: context.state.workspaceId,
13265
+ deploymentId,
13266
+ stack,
13267
+ placement: {
13268
+ hostId: context.state.hostId,
13269
+ memoryReservationBytes: 1024 * 1024 * 1024
13270
+ }
13271
+ },
13272
+ maxAttempts: 1
13273
+ })
13274
+ });
13275
+ const published = await wait(context.apiUrl, context.apiKey, publish.operation.id, 60 * 60000);
13276
+ artifactImage = String(published.result?.image ?? "");
13277
+ if (!/@sha256:[a-f0-9]{64}$/.test(artifactImage))
13278
+ throw new Error("build artifact digest is invalid");
13279
+ }
13280
+ let operation;
13281
+ try {
13282
+ const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13283
+ method: "POST",
13284
+ headers: {
13285
+ "idempotency-key": `preview:${deploymentId}:deploy`
13286
+ },
13287
+ body: JSON.stringify({
13288
+ kind: "workspace.service.deploy",
13289
+ stack: context.state.repository,
13290
+ environmentId: context.state.environment,
13291
+ environmentSnapshotId,
13292
+ payload: {
13293
+ workspaceId: context.state.workspaceId,
13294
+ deploymentId,
13295
+ sandboxId: context.state.sandboxId,
13296
+ runtimeType: preview.deploymentType,
13297
+ stack,
13298
+ workspace: preview.workspace,
13299
+ artifactImage,
13300
+ targetHost,
13301
+ serviceUrls,
13302
+ services: preview.services,
13303
+ placement: {
13304
+ hostId: deploymentTarget.hostId,
13305
+ requiredWorkerRole: "runtime",
13306
+ pool: deploymentTarget.pool,
13307
+ memoryReservationBytes: serviceMemoryReservation(preview.services)
13308
+ }
13309
+ },
13310
+ maxAttempts: 3
13311
+ })
13312
+ });
13313
+ operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
13314
+ } catch (error) {
13315
+ const message = error instanceof Error ? error.message : String(error);
13316
+ throw new Error(`${message}; retry with ` + "`sandblocks sandbox deploy . --resume`");
13317
+ }
13318
+ context.state.deploymentId = deploymentId;
13319
+ await writeState(context.file, context.state);
13320
+ const routes = operation.result?.serviceRoutes ?? [];
13321
+ const images = operation.result?.images ?? [];
13322
+ if (typeof preview.ssoEnabled === "boolean") {
13323
+ 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 }) });
13324
+ }
13325
+ let sandbox = await recordSandbox(context, loaded, preview, routes, operation.result?.healthEvidence ?? [], images, preview.steps.length ? "active" : "available");
13326
+ let checkEvidence = [];
13327
+ try {
13328
+ if (preview.steps.length) {
13329
+ const checkRoutes = sandbox.previewUrls ?? [];
13330
+ await waitForPublishedRoutes(checkRoutes);
13331
+ const serviceUrls2 = Object.fromEntries(checkRoutes.flatMap((route) => route.service ? [
13332
+ [
13333
+ `SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`,
13334
+ route.url
13335
+ ]
13336
+ ] : []));
13337
+ const check = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13338
+ method: "POST",
13339
+ headers: {
13340
+ "idempotency-key": `preview:${deploymentId}:checks`
13341
+ },
13342
+ body: JSON.stringify({
13343
+ kind: "workspace.check.run",
13344
+ stack: context.state.repository,
13345
+ environmentId: context.state.environment,
13346
+ environmentSnapshotId,
13347
+ payload: {
13348
+ workspaceId: buildWorkspaceId ?? context.state.workspaceId,
13349
+ deploymentId,
13350
+ sandboxId: context.state.sandboxId,
13351
+ runtimeType: preview.deploymentType,
13352
+ checks: preview.steps,
13353
+ serviceUrls: serviceUrls2,
13354
+ placement: { hostId: buildHostId ?? context.state.hostId }
13355
+ },
13356
+ maxAttempts: 1
13357
+ })
13358
+ });
13359
+ const checked2 = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
13360
+ checkEvidence = checked2.result?.checks ?? [];
13361
+ if (checked2.result?.passed !== true) {
13362
+ throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
12777
13363
  }
12778
- } finally {
12779
- if (idleTimer)
12780
- clearTimeout(idleTimer);
12781
- if (completionTimer)
12782
- clearTimeout(completionTimer);
12783
- if (lineBuffer)
12784
- await this.emitLine(lineBuffer, "stdout", context);
13364
+ sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
12785
13365
  }
13366
+ } catch (error) {
13367
+ await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
13368
+ return;
13369
+ });
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
+ throw error;
12786
13378
  }
12787
- result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
12788
- const sessionId = this.options.sessionId?.(stdout, stderr);
12789
- return {
12790
- iteration: context.iteration,
12791
- exitCode,
12792
- stdout,
12793
- stderr,
12794
- ...sessionId ? { sessionId } : {},
12795
- ...completionSignal ? { completionSignal } : {},
12796
- startedAt,
12797
- finishedAt: new Date().toISOString()
12798
- };
12799
- }
12800
- async emitLine(line, stream, context) {
12801
- const timestamp = new Date().toISOString();
12802
- const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
12803
- const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
12804
- {
12805
- type: "raw",
12806
- line: stream === "stderr" ? `[stderr] ${line}` : line,
12807
- iteration: context.iteration,
12808
- timestamp
12809
- }
12810
- ];
12811
- for (const event of events)
12812
- await context.emit(event);
12813
- }
13379
+ if (buildWorkspaceId)
13380
+ await destroyDetachedWorkspace(context, buildWorkspaceId, `preview:${deploymentId}:build-cleanup`);
13381
+ await releaseEnvironmentSnapshot(context, environmentSnapshotId);
13382
+ context.state.deploymentId = deploymentId;
13383
+ context.state.candidate = undefined;
13384
+ const previewUrls = sandbox.previewUrls ?? [];
13385
+ context.state.previewUrls = previewUrls;
13386
+ await writeState(context.file, context.state);
13387
+ console.log(`deployment ${deploymentId} available`);
13388
+ for (const route of previewUrls)
13389
+ console.log(`${route.service ?? "service"} ${route.url}`);
12814
13390
  }
12815
-
12816
- class ClaudeCodeAgent extends CommandAgent {
12817
- constructor(options) {
12818
- super({
12819
- id: "claude-code",
12820
- env: credentialEnvironment(options.env, options.credential),
12821
- prompt: "argument",
12822
- command: (context) => [
12823
- options.binary ?? "claude",
12824
- "--print",
12825
- "--model",
12826
- options.model,
12827
- ...options.effort ? ["--effort", options.effort] : [],
12828
- ...options.extraArgs ?? []
12829
- ],
12830
- sessionId: extractSessionId,
12831
- parseEvent: parseJsonAgentEvent
13391
+ async function redeploy(args, dependencies) {
13392
+ const context = await getContext(args, dependencies);
13393
+ const routing = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-aliases`);
13394
+ if (Array.isArray(routing.aliases) && routing.aliases.some((alias) => alias.sandboxId === context.state.sandboxId)) {
13395
+ throw new Error("promoted sandboxes are immutable; create a new sandbox and promote it instead");
13396
+ }
13397
+ await renewLease(context, dependencies);
13398
+ const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
13399
+ const previousDeploymentId = context.state.deploymentId;
13400
+ const previousUrls = context.state.previewUrls;
13401
+ const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...candidateState } = context.state;
13402
+ context.state = candidateState;
13403
+ await writeState(context.file, context.state);
13404
+ try {
13405
+ await destroyWorkspace(context, `${context.state.sandboxId}:workspace-redeploy-destroy:${Date.now()}`);
13406
+ const imported = await importSource({
13407
+ root: context.root,
13408
+ projectId: context.state.projectId,
13409
+ sandboxId: context.state.sandboxId,
13410
+ workspaceId: context.state.workspaceId,
13411
+ pool: dependencies.option(context.options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "build-fleet",
13412
+ apiUrl: context.apiUrl,
13413
+ apiKey: context.apiKey,
13414
+ idempotencyKey: `${context.state.sandboxId}:workspace-redeploy:${Date.now()}`,
13415
+ source: dependencies.source,
13416
+ ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
12832
13417
  });
13418
+ const hostId = String(imported.operation.assignedHostId ?? "");
13419
+ if (!hostId)
13420
+ throw new Error("workspace re-import did not bind a sandbox host");
13421
+ context.state.hostId = hostId;
13422
+ await writeState(context.file, context.state);
13423
+ await deploy(args, dependencies);
13424
+ if (previousDeploymentId)
13425
+ await destroyDeploymentById(context, previousDeploymentId);
13426
+ } catch (error) {
13427
+ const failedCandidateId = context.state.deploymentId;
13428
+ if (failedCandidateId && failedCandidateId !== previousDeploymentId) {
13429
+ await destroyDeploymentById(context, failedCandidateId).catch(() => {
13430
+ return;
13431
+ });
13432
+ }
13433
+ context.state.deploymentId = previousDeploymentId;
13434
+ context.state.previewUrls = previousUrls;
13435
+ await writeState(context.file, context.state);
13436
+ throw error;
12833
13437
  }
12834
13438
  }
12835
-
12836
- class CodexAgent extends CommandAgent {
12837
- constructor(options) {
12838
- super({
12839
- id: "codex",
12840
- env: credentialEnvironment(options.env, options.credential),
12841
- prompt: "argument",
12842
- command: (context) => [
12843
- options.binary ?? "codex",
12844
- "exec",
12845
- "--model",
12846
- options.model,
12847
- ...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
12848
- ...options.extraArgs ?? []
12849
- ],
12850
- sessionId: extractSessionId,
12851
- parseEvent: parseJsonAgentEvent
13439
+ async function renew(args, dependencies) {
13440
+ const context = await getContext(args, dependencies);
13441
+ const lease = await renewLease(context, dependencies);
13442
+ console.log(`lease ${lease.lease.id} renewed until ${lease.lease.expiresAt}`);
13443
+ }
13444
+ async function renewLease(context, dependencies) {
13445
+ const previousLeaseId = context.state.leaseId;
13446
+ const lease = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}/leases`, {
13447
+ method: "POST",
13448
+ body: JSON.stringify({
13449
+ subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
13450
+ scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
13451
+ ttlSeconds: Number(dependencies.option(context.options, "ttl-seconds") ?? 28800)
13452
+ })
13453
+ });
13454
+ context.state.leaseId = lease.lease.id;
13455
+ context.state.leaseToken = lease.token;
13456
+ await writeState(context.file, context.state);
13457
+ if (previousLeaseId && previousLeaseId !== lease.lease.id) {
13458
+ await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}/leases/${previousLeaseId}`, { method: "DELETE" }).catch(() => {
13459
+ return;
12852
13460
  });
12853
13461
  }
13462
+ return lease;
12854
13463
  }
12855
-
12856
- class PiAgent extends CommandAgent {
12857
- constructor(options) {
12858
- super({
12859
- id: "pi",
12860
- env: credentialEnvironment(options.env, options.credential),
12861
- prompt: "argument",
12862
- command: (context) => [
12863
- options.binary ?? "pi",
12864
- "--print",
12865
- "--model",
12866
- options.model,
12867
- ...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
12868
- ...options.extraArgs ?? []
13464
+ async function exec(args, dependencies) {
13465
+ const separator = args.indexOf("--");
13466
+ if (separator < 0 || separator === args.length - 1)
13467
+ throw new Error("sandbox exec requires -- followed by a command");
13468
+ const context = await getContext(args.slice(0, separator), dependencies);
13469
+ const command = args.slice(separator + 1);
13470
+ const operation = await dataOperation(context, "exec", { executable: command[0], args: command.slice(1) });
13471
+ printOutput(operation);
13472
+ }
13473
+ async function git2(args, dependencies) {
13474
+ const actionIndex = args.findIndex((value) => ["status", "diff", "stage", "checkout"].includes(value));
13475
+ if (actionIndex < 0)
13476
+ throw new Error("sandbox git action must be status, diff, stage, or checkout");
13477
+ const context = await getContext(args.slice(0, actionIndex), dependencies);
13478
+ const action = args[actionIndex];
13479
+ if (!action)
13480
+ throw new Error("sandbox git action is required");
13481
+ const rest = args.slice(actionIndex + 1);
13482
+ const operation = await dataOperation(context, "git", action === "checkout" ? { action, branch: rest[0] } : { action, ...rest.length ? { paths: rest } : {} });
13483
+ printOutput(operation);
13484
+ }
13485
+ async function down(args, dependencies) {
13486
+ const context = await getContext(args, dependencies);
13487
+ await destroyDeployment(context);
13488
+ await destroyWorkspace(context, `${context.state.sandboxId}:workspace-destroy`).catch(() => {
13489
+ return;
13490
+ });
13491
+ await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`, { method: "DELETE" }).catch(() => {
13492
+ return;
13493
+ });
13494
+ await rm4(context.file, { force: true });
13495
+ console.log(`sandbox ${context.state.sandboxId} destroyed`);
13496
+ }
13497
+ async function resolveDeploymentTarget(context, fallback) {
13498
+ 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
+ ]);
13503
+ const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
13504
+ const requestedPool = process.env.SANDBLOCKS_DEPLOYMENT_WORKER_POOL?.trim() || process.env.SANDBLOCKS_RUNTIME_WORKER_POOL?.trim();
13505
+ 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)
13508
+ 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;
13512
+ const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
13513
+ 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") };
13516
+ }
13517
+ async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
13518
+ const deploymentId = context.state.deploymentId;
13519
+ if (!deploymentId)
13520
+ throw new Error("deployment ID is required when recording sandbox routes");
13521
+ const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
13522
+ method: "POST",
13523
+ body: JSON.stringify({
13524
+ id: context.state.sandboxId,
13525
+ goal: preview.goal,
13526
+ status: status3,
13527
+ stack: loaded.manifest.name ?? context.state.repository,
13528
+ deploymentType: preview.deploymentType,
13529
+ revisionId: deploymentId,
13530
+ generation: Date.now(),
13531
+ baseDomain: preview.baseDomain,
13532
+ domains: await managedServiceDomains(context, loaded.manifest.name ?? context.state.repository, preview, deploymentId),
13533
+ stableDomains: preview.stableDomains,
13534
+ serviceRoutes,
13535
+ healthEvidence,
13536
+ images,
13537
+ volumes: [
13538
+ {
13539
+ name: `${context.state.repository}-workspace`,
13540
+ target: "/workspace",
13541
+ access: "read-write"
13542
+ }
12869
13543
  ],
12870
- sessionId: extractSessionId,
12871
- parseEvent: parseJsonAgentEvent
12872
- });
12873
- }
13544
+ repositories: [context.state.repository],
13545
+ executor: "sandblocks"
13546
+ })
13547
+ });
13548
+ return body.sandbox;
12874
13549
  }
12875
- function parseJsonAgentEvent(line, context) {
12876
- let value;
12877
- try {
12878
- value = JSON.parse(line);
12879
- } catch {
12880
- return { type: "raw", line, ...context };
12881
- }
12882
- const type = String(value.type ?? "");
12883
- if (/tool/i.test(type)) {
12884
- return {
12885
- type: "toolCall",
12886
- name: String(value.name ?? value.tool ?? value.tool_name ?? "tool"),
12887
- ...value.input !== undefined ? { input: value.input } : {},
12888
- ...context
12889
- };
13550
+ async function createEnvironmentSnapshot(context) {
13551
+ const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots`, {
13552
+ method: "POST",
13553
+ body: JSON.stringify({
13554
+ stack: context.state.repository,
13555
+ app: "*",
13556
+ environmentId: context.state.environment
13557
+ })
13558
+ });
13559
+ const id2 = body.snapshot?.id;
13560
+ if (typeof id2 !== "string" || !/^[a-f0-9-]{36}$/.test(id2)) {
13561
+ throw new Error("managed environment snapshot response is invalid");
12890
13562
  }
12891
- const text2 = value.text ?? value.message ?? value.content;
12892
- return typeof text2 === "string" ? { type: "text", text: text2, ...context } : { type: "raw", line, ...context };
12893
- }
12894
- function credentialEnvironment(env, credential) {
12895
- if (!credential)
12896
- return env;
12897
- if (!/^[A-Z][A-Z0-9_]*$/.test(credential.environment))
12898
- throw new Error("credential environment name is invalid");
12899
- const value = process.env[credential.environment];
12900
- if (value === undefined)
12901
- return env;
12902
- const target = credential.target ?? credential.environment;
12903
- if (!/^[A-Z][A-Z0-9_]*$/.test(target))
12904
- throw new Error("credential target name is invalid");
12905
- return { ...env, [target]: value };
13563
+ return id2;
12906
13564
  }
12907
- function extractSessionId(stdout, stderr) {
12908
- const match = `${stdout}
12909
- ${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
12910
- return match?.[1];
13565
+ async function releaseEnvironmentSnapshot(context, snapshotId) {
13566
+ await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
12911
13567
  }
12912
- var Output = {
12913
- string(input) {
12914
- return { kind: "string", tag: validTag(input.tag) };
12915
- },
12916
- object(input) {
12917
- return { kind: "object", tag: validTag(input.tag), schema: input.schema };
12918
- }
12919
- };
12920
- async function extractOutput(spec, text2) {
12921
- const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
12922
- const match = text2.match(pattern);
12923
- if (!match)
12924
- throw new Error(`structured output tag <${spec.tag}> was not found`);
12925
- const content = match[1]?.trim();
12926
- if (content === undefined)
12927
- throw new Error(`structured output tag <${spec.tag}> is empty`);
12928
- if (spec.kind === "string")
12929
- return content;
12930
- let value;
12931
- try {
12932
- value = JSON.parse(content);
12933
- } catch {
12934
- throw new Error(`structured output <${spec.tag}> is not valid JSON`);
12935
- }
12936
- const schema = spec.schema;
12937
- if (schema["~standard"]) {
12938
- const result = await schema["~standard"].validate(value);
12939
- if (result.issues?.length)
12940
- throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
12941
- return result.value;
12942
- }
12943
- if (schema.safeParse) {
12944
- const result = schema.safeParse(value);
12945
- if (!result.success)
12946
- throw new Error(`structured output validation failed: ${String(result.error)}`);
12947
- return result.data;
12948
- }
12949
- if (schema.parse)
12950
- return schema.parse(value);
12951
- throw new Error("structured output schema is unsupported");
13568
+ async function destroyDeployment(context) {
13569
+ if (!context.state.deploymentId)
13570
+ return;
13571
+ await destroyDeploymentById(context, context.state.deploymentId);
13572
+ const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...remainingState } = context.state;
13573
+ context.state = remainingState;
13574
+ await writeState(context.file, context.state);
12952
13575
  }
12953
- function validTag(tag) {
12954
- if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tag))
12955
- throw new Error("output tag is invalid");
12956
- return tag;
13576
+ async function destroyDeploymentById(context, deploymentId) {
13577
+ const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
13578
+ method: "POST",
13579
+ headers: { "idempotency-key": `preview:${deploymentId}:destroy:${Date.now()}` },
13580
+ body: JSON.stringify({
13581
+ kind: "workspace.service.destroy",
13582
+ payload: {
13583
+ deploymentId,
13584
+ placement: { hostId: context.state.deploymentHostId ?? context.state.hostId }
13585
+ },
13586
+ maxAttempts: 1
13587
+ })
13588
+ });
13589
+ await wait(context.apiUrl, context.apiKey, body.operation.id);
12957
13590
  }
12958
- function escapeRegex(value) {
12959
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13591
+ async function destroyWorkspace(context, idempotencyKey) {
13592
+ await destroyDetachedWorkspace(context, context.state.workspaceId, idempotencyKey);
12960
13593
  }
12961
- async function executeProcess(command, options = {}) {
12962
- if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
12963
- throw new Error("command must contain safe non-empty arguments");
12964
- }
12965
- const startedAt = new Date().toISOString();
12966
- const [executable, ...args] = command;
12967
- if (!executable)
12968
- throw new Error("command executable is required");
12969
- const child = spawn(executable, args, {
12970
- cwd: options.cwd ?? options.hostCwd,
12971
- env: options.env ? { ...process.env, ...options.env } : process.env,
12972
- stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
13594
+ async function destroyDetachedWorkspace(context, workspaceId, idempotencyKey) {
13595
+ const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(workspaceId)}`, {
13596
+ method: "DELETE",
13597
+ headers: {
13598
+ "content-type": "application/json",
13599
+ "x-sandblocks-api-key": context.apiKey,
13600
+ "idempotency-key": idempotencyKey
13601
+ },
13602
+ body: JSON.stringify({ idempotencyKey })
12973
13603
  });
12974
- if (options.stdin !== undefined)
12975
- child.stdin?.end(options.stdin);
12976
- let timedOut = false;
12977
- let stdout = "";
12978
- let stderr = "";
12979
- let callbackQueue = Promise.resolve();
12980
- child.stdout?.on("data", (chunk) => {
12981
- const text2 = chunk.toString();
12982
- stdout = `${stdout}${text2}`.slice(-4 * 1024 * 1024);
12983
- callbackQueue = callbackQueue.then(() => options.onStdout?.(text2)).then(() => {
12984
- return;
12985
- });
13604
+ const body = await response.json().catch(() => ({}));
13605
+ if (response.ok)
13606
+ await wait(context.apiUrl, context.apiKey, body.operation.id);
13607
+ else if (![404, 409].includes(response.status))
13608
+ throw new Error(String(body.error ?? "workspace destroy failed"));
13609
+ }
13610
+ async function importSource(input) {
13611
+ const source = await input.source(input.root);
13612
+ const response = await fetchWithRetry(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
13613
+ method: "POST",
13614
+ headers: {
13615
+ "content-type": "application/x-tar",
13616
+ "content-length": String(source.bundle.byteLength),
13617
+ "x-sandblocks-api-key": input.apiKey,
13618
+ "x-sandblocks-workspace-id": input.workspaceId,
13619
+ ...input.sandboxId ? { "x-sandblocks-sandbox-id": input.sandboxId } : {},
13620
+ "x-sandblocks-worker-pool": input.pool,
13621
+ ...input.workspaceImage ? { "x-sandblocks-workspace-image": input.workspaceImage } : {},
13622
+ "idempotency-key": input.idempotencyKey
13623
+ },
13624
+ body: new Blob([Uint8Array.from(source.bundle)])
12986
13625
  });
12987
- child.stderr?.on("data", (chunk) => {
12988
- const text2 = chunk.toString();
12989
- stderr = `${stderr}${text2}`.slice(-4 * 1024 * 1024);
12990
- callbackQueue = callbackQueue.then(() => options.onStderr?.(text2)).then(() => {
12991
- return;
12992
- });
13626
+ const submitted = await response.json().catch(() => ({}));
13627
+ if (!response.ok)
13628
+ throw new Error(String(submitted?.error ?? `sandbox import failed (${response.status})`));
13629
+ return {
13630
+ files: source.files,
13631
+ bytes: source.bundle.byteLength,
13632
+ operation: await wait(input.apiUrl, input.apiKey, submitted.operation.id)
13633
+ };
13634
+ }
13635
+ async function dataOperation(context, action, body) {
13636
+ context.state.sequence += 1;
13637
+ await writeState(context.file, context.state);
13638
+ const submitted = await leaseRequest(context, `/v1/sandboxes/${context.state.sandboxId}/workspaces/${context.state.repository}/${action}`, {
13639
+ method: "POST",
13640
+ headers: { "idempotency-key": `cli:${context.state.sandboxId}:${context.state.sequence}:${action}` },
13641
+ body: JSON.stringify(body)
12993
13642
  });
12994
- const stop = () => child.kill("SIGTERM");
12995
- options.signal?.addEventListener("abort", stop, { once: true });
12996
- const timer = options.timeoutMs ? setTimeout(() => {
12997
- timedOut = true;
12998
- child.kill("SIGKILL");
12999
- }, options.timeoutMs) : undefined;
13643
+ return waitLease(context, submitted.operation.id);
13644
+ }
13645
+ async function getContext(args, dependencies) {
13646
+ const options = dependencies.parse(args);
13647
+ const root = path5.resolve(options.positionals[0] ?? process.cwd());
13648
+ await loadLocalEnvironment(root);
13649
+ const environment = dependencies.option(options, "environment") ?? "preview";
13650
+ const file = statePath(root, environment);
13651
+ let state;
13000
13652
  try {
13001
- const exitCode = await new Promise((resolve3, reject) => {
13002
- child.once("error", reject);
13003
- child.once("close", (code, signal) => resolve3(code ?? (signal ? 128 : 1)));
13004
- });
13005
- await callbackQueue;
13006
- if (options.signal?.aborted)
13007
- throw options.signal.reason ?? new Error("operation aborted");
13008
- return {
13009
- exitCode: timedOut ? 124 : exitCode,
13010
- stdout,
13011
- stderr,
13012
- timedOut,
13013
- startedAt,
13014
- finishedAt: new Date().toISOString()
13015
- };
13016
- } finally {
13017
- if (timer)
13018
- clearTimeout(timer);
13019
- options.signal?.removeEventListener("abort", stop);
13653
+ state = JSON.parse(await readFile5(file, "utf8"));
13654
+ } catch {
13655
+ throw new Error(`sandbox '${environment}' does not exist; use sandbox create or up`);
13020
13656
  }
13657
+ const configured = client(options, dependencies.option, state.projectId);
13658
+ return { root, file, options, state, ...configured };
13021
13659
  }
13022
- class SandblocksSandbox {
13023
- runtime;
13024
- options;
13025
- git;
13026
- id;
13027
- provider;
13028
- cwd;
13029
- failed = false;
13030
- disposed = false;
13031
- eventQueue = [];
13032
- eventWaiters = [];
13033
- constructor(runtime, options, git3) {
13034
- this.runtime = runtime;
13035
- this.options = options;
13036
- this.git = git3;
13037
- this.id = runtime.id;
13038
- this.provider = runtime.provider;
13039
- this.cwd = runtime.cwd;
13040
- }
13041
- exec(command, options = {}) {
13042
- return this.runtime.exec(command, options);
13043
- }
13044
- upload(source, destination) {
13045
- if (!this.runtime.upload)
13046
- throw new Error(`${this.provider} provider does not support upload`);
13047
- return this.runtime.upload(source, destination);
13048
- }
13049
- download(source, destination) {
13050
- if (!this.runtime.download)
13051
- throw new Error(`${this.provider} provider does not support download`);
13052
- return this.runtime.download(source, destination);
13660
+ function selectPreview(loaded, id2) {
13661
+ return resolveEnvironment(loaded.manifest, id2);
13662
+ }
13663
+ function client(options, option2, projectId) {
13664
+ const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
13665
+ const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY ?? "";
13666
+ if (!apiUrl || !apiKey)
13667
+ throw new Error("SANDBLOCKS_API_URL and SANDBLOCKS_API_KEY are required");
13668
+ return { apiUrl, apiKey, projectId };
13669
+ }
13670
+ async function request(apiUrl, apiKey, pathname, init = {}) {
13671
+ const headers = new Headers(init.headers);
13672
+ headers.set("x-sandblocks-api-key", apiKey);
13673
+ if (init.body)
13674
+ headers.set("content-type", "application/json");
13675
+ const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
13676
+ const body = await response.json().catch(() => ({}));
13677
+ if (!response.ok)
13678
+ throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
13679
+ return body;
13680
+ }
13681
+ async function leaseRequest(context, pathname, init = {}) {
13682
+ const headers = new Headers(init.headers);
13683
+ headers.set("authorization", `Bearer ${context.state.leaseToken}`);
13684
+ if (init.body)
13685
+ headers.set("content-type", "application/json");
13686
+ const response = await fetchWithRetry(`${context.apiUrl}${pathname}`, { ...init, headers });
13687
+ const body = await response.json().catch(() => ({}));
13688
+ if (!response.ok)
13689
+ throw new Error(String(body.error ?? `Sandblocks lease request failed (${response.status})`));
13690
+ return body;
13691
+ }
13692
+ async function waitForPublishedRoutes(routes, timeoutMs = 240000) {
13693
+ const deadline = Date.now() + timeoutMs;
13694
+ let pending = routes.map((route) => ({
13695
+ label: route.url,
13696
+ url: route.service === "api" ? new URL("/health", route.url).toString() : route.url
13697
+ }));
13698
+ while (pending.length && Date.now() < deadline) {
13699
+ const checks = await Promise.all(pending.map(async (route) => {
13700
+ try {
13701
+ const response = await fetch(route.url, { redirect: "manual" });
13702
+ await response.body?.cancel().catch(() => {
13703
+ return;
13704
+ });
13705
+ return response.status >= 200 && response.status < 400 ? undefined : route;
13706
+ } catch {
13707
+ return route;
13708
+ }
13709
+ }));
13710
+ pending = checks.filter((route) => typeof route === "object" && route !== null);
13711
+ if (pending.length)
13712
+ await Bun.sleep(2000);
13053
13713
  }
13054
- stop(reason) {
13055
- return this.runtime.stop(reason);
13714
+ if (pending.length) {
13715
+ throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
13056
13716
  }
13057
- async* events(options = {}) {
13058
- while (!this.disposed || this.eventQueue.length) {
13059
- if (options.signal?.aborted)
13717
+ }
13718
+ async function fetchWithRetry(url, init = {}) {
13719
+ const method = (init.method ?? "GET").toUpperCase();
13720
+ const headers = new Headers(init.headers);
13721
+ const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
13722
+ const attempts = safeToRetry ? 8 : 1;
13723
+ let lastError;
13724
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
13725
+ try {
13726
+ const response = await fetch(url, init);
13727
+ if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
13728
+ return response;
13729
+ await response.body?.cancel().catch(() => {
13060
13730
  return;
13061
- const queued = this.eventQueue.shift();
13062
- if (queued) {
13063
- yield queued;
13064
- continue;
13065
- }
13066
- const event = await new Promise((resolve22) => {
13067
- const abort = () => resolve22(undefined);
13068
- options.signal?.addEventListener("abort", abort, { once: true });
13069
- this.eventWaiters.push((value) => {
13070
- options.signal?.removeEventListener("abort", abort);
13071
- resolve22(value);
13072
- });
13073
13731
  });
13074
- if (!event)
13075
- return;
13076
- yield event;
13077
- }
13078
- }
13079
- async run(options) {
13080
- if (options.output && (options.maxIterations ?? 1) !== 1)
13081
- throw new Error("structured output requires maxIterations === 1");
13082
- const startedAt = new Date().toISOString();
13083
- const prompt = await resolvePrompt(options);
13084
- const signals = array(options.completionSignal ?? "<promise>COMPLETE</promise>");
13085
- const logger = createLogger(options.logging, this.git.hostCwd);
13086
- const baseline = await gitHead(this.git.runtimeCwd);
13087
- const iterations = [];
13088
- try {
13089
- await runSandboxHooks(this.runtime, options.hooks?.sandbox?.beforeAgent);
13090
- for (let iteration = 1;iteration <= (options.maxIterations ?? 1); iteration += 1) {
13091
- const result = await options.agent.run({
13092
- runtime: this.runtime,
13093
- prompt,
13094
- iteration,
13095
- signal: options.signal,
13096
- idleTimeoutSeconds: options.idleTimeoutSeconds ?? 600,
13097
- completionTimeoutSeconds: options.completionTimeoutSeconds ?? 60,
13098
- completionSignal: signals,
13099
- emit: async (event) => {
13100
- this.publish(event);
13101
- await logger(event);
13102
- }
13103
- });
13104
- iterations.push(result);
13105
- if (result.exitCode !== 0)
13106
- throw new Error(result.stderr || `${options.agent.id} exited ${result.exitCode}`);
13107
- if (result.completionSignal)
13108
- break;
13109
- }
13110
- await runSandboxHooks(this.runtime, options.hooks?.sandbox?.afterAgent);
13111
- await this.runtime.syncToHost?.();
13112
- const commits = await collectCommits(this.git.runtimeCwd, baseline, this.options.timeouts?.commitCollectionMs);
13113
- if (this.git.mergeToHead && this.git.branch)
13114
- await mergeBranch(this.git, this.options.timeouts?.mergeToHostMs);
13115
- const allOutput = iterations.map((item) => item.stdout).join(`
13116
- `);
13117
- const completionSignal = iterations.find((item) => item.completionSignal)?.completionSignal;
13118
- return {
13119
- sandboxId: this.id,
13120
- provider: this.provider,
13121
- iterations,
13122
- commits,
13123
- ...this.git.branch ? { branch: this.git.branch } : {},
13124
- ...completionSignal ? { completionSignal } : {},
13125
- ...options.output ? { output: await extractOutput(options.output, allOutput) } : {},
13126
- startedAt,
13127
- finishedAt: new Date().toISOString()
13128
- };
13129
13732
  } catch (error) {
13130
- this.failed = true;
13131
- throw error;
13132
- }
13133
- }
13134
- async destroy() {
13135
- if (this.disposed)
13136
- return;
13137
- this.disposed = true;
13138
- await runHostHooks(this.options.hooks?.host?.beforeDestroy, this.git.hostCwd);
13139
- const retention = this.options.retention ?? "destroy";
13140
- if (retention === "destroy" || retention === "on-failure" && !this.failed)
13141
- await this.runtime.destroy();
13142
- for (const waiter of this.eventWaiters.splice(0))
13143
- waiter(undefined);
13144
- if (this.git.worktree && retention !== "always") {
13145
- await executeProcess([
13146
- "git",
13147
- "-C",
13148
- this.git.hostCwd,
13149
- "worktree",
13150
- "remove",
13151
- "--force",
13152
- this.git.worktree
13153
- ]);
13154
- await rm4(this.git.worktree, { recursive: true, force: true });
13733
+ lastError = error;
13734
+ if (attempt === attempts - 1)
13735
+ throw error;
13155
13736
  }
13737
+ await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
13156
13738
  }
13157
- async[Symbol.asyncDispose]() {
13158
- await this.destroy();
13159
- }
13160
- publish(event) {
13161
- const waiter = this.eventWaiters.shift();
13162
- if (waiter)
13163
- waiter(event);
13164
- else
13165
- this.eventQueue.push(event);
13166
- }
13739
+ throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
13167
13740
  }
13168
- async function createSandbox(options) {
13169
- const provider = options.provider ?? options.sandbox;
13170
- if (!provider)
13171
- throw new Error("sandbox provider is required");
13172
- assertProviderCapabilities(provider, options);
13173
- const hostCwd = resolve(options.cwd ?? process.cwd());
13174
- await runHostHooks(options.hooks?.host?.beforeCreate, hostCwd);
13175
- const branchStrategy = options.branch ?? options.branchStrategy ?? { type: "head" };
13176
- if (options.copyToWorktree?.length && branchStrategy.type === "head") {
13177
- throw new Error("copyToWorktree is not supported with the head branch strategy");
13178
- }
13179
- const git3 = await prepareGit(hostCwd, branchStrategy, options.timeouts?.gitSetupMs);
13180
- await runHostHooks(options.hooks?.host?.onWorktreeReady, git3.runtimeCwd);
13181
- for (const file of options.copyToWorktree ?? []) {
13182
- const source = resolve(hostCwd, file);
13183
- const destination = resolve(git3.runtimeCwd, file);
13184
- await mkdir4(dirname(destination), { recursive: true });
13185
- await withTimeout(cp(source, destination, { recursive: true }), options.timeouts?.copyToWorktreeMs ?? 60000, `copying '${file}'`);
13186
- }
13187
- const id2 = options.id ?? randomUUID2();
13188
- const runtime = await provider.create({
13189
- id: id2,
13190
- cwd: git3.runtimeCwd,
13191
- image: options.image,
13192
- env: options.env,
13193
- mounts: options.mounts,
13194
- network: options.network,
13195
- groups: options.groups,
13196
- devices: options.devices,
13197
- cpus: options.cpus,
13198
- memoryMb: options.memoryMb,
13199
- pids: options.pids,
13200
- tmpfsMb: options.tmpfsMb,
13201
- signal: options.signal,
13202
- idempotencyKey: options.idempotencyKey,
13203
- metadata: options.metadata
13204
- });
13205
- const sandbox = new SandblocksSandbox(runtime, options, git3);
13206
- try {
13207
- await runHostHooks(options.hooks?.host?.onSandboxReady, hostCwd);
13208
- await runHostHooks(options.hooks?.host?.afterCreate, hostCwd);
13209
- await runSandboxHooks(runtime, options.hooks?.sandbox?.onSandboxReady);
13210
- await runSandboxHooks(runtime, options.hooks?.sandbox?.afterCreate);
13211
- return sandbox;
13212
- } catch (error) {
13213
- await sandbox.destroy().catch(() => {
13214
- return;
13215
- });
13216
- throw error;
13741
+ async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
13742
+ const deadline = Date.now() + timeoutMs;
13743
+ while (Date.now() < deadline) {
13744
+ const body = await request(apiUrl, apiKey, `/v1/operations/${operationId}`);
13745
+ if (body.operation.state === "succeeded")
13746
+ return body.operation;
13747
+ if (["failed", "cancelled"].includes(body.operation.state))
13748
+ throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
13749
+ await Bun.sleep(500);
13217
13750
  }
13751
+ throw new Error(`operation ${operationId} timed out`);
13218
13752
  }
13219
- async function connectSandbox(options) {
13220
- if (!options.provider.reconnect)
13221
- throw new Error(`${options.provider.kind} provider does not support reconnect`);
13222
- const cwd = resolve(options.cwd ?? process.cwd());
13223
- const runtime = await options.provider.reconnect({ id: options.id, cwd });
13224
- return new SandblocksSandbox(runtime, { provider: options.provider, cwd, retention: options.retention ?? "always" }, { hostCwd: cwd, runtimeCwd: cwd });
13753
+ async function waitLease(context, operationId) {
13754
+ const deadline = Date.now() + 3 * 60000;
13755
+ while (Date.now() < deadline) {
13756
+ const body = await leaseRequest(context, `/v1/sandbox-operations/${operationId}`);
13757
+ if (body.operation.state === "succeeded")
13758
+ return body.operation;
13759
+ if (["failed", "cancelled"].includes(body.operation.state))
13760
+ throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
13761
+ await Bun.sleep(500);
13762
+ }
13763
+ throw new Error(`sandbox operation ${operationId} timed out`);
13225
13764
  }
13226
- async function run(options) {
13227
- const sandbox = await createSandbox(options);
13765
+ async function loadLocalEnvironment(root) {
13766
+ const file = path5.join(root, ".sandblocks", "config.env");
13767
+ let contents;
13228
13768
  try {
13229
- return await sandbox.run(options);
13230
- } finally {
13231
- await sandbox.destroy();
13769
+ contents = await readFile5(file, "utf8");
13770
+ } catch {
13771
+ return;
13232
13772
  }
13233
- }
13234
- async function resolvePrompt(options) {
13235
- if (Boolean(options.prompt) === Boolean(options.promptFile))
13236
- throw new Error("provide exactly one of prompt or promptFile");
13237
- const promptFile = options.promptFile;
13238
- let prompt = options.prompt ?? await readFile5(resolve(process.cwd(), promptFile ?? ""), "utf8");
13239
- for (const [key, value] of Object.entries(options.promptArgs ?? {}))
13240
- prompt = prompt.replaceAll(`{{${key}}}`, value);
13241
- return prompt;
13242
- }
13243
- async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
13244
- if (strategy.type === "head")
13245
- return { hostCwd, runtimeCwd: hostCwd };
13246
- const originalHead = await gitHead(hostCwd);
13247
- if (strategy.type === "branch") {
13248
- await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
13249
- return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
13773
+ for (const raw of contents.split(/\r?\n/)) {
13774
+ const line = raw.trim();
13775
+ if (!line || line.startsWith("#") || !line.includes("="))
13776
+ continue;
13777
+ const [key, ...parts] = line.split("=");
13778
+ if (!key?.startsWith("SANDBLOCKS_") || process.env[key] !== undefined)
13779
+ continue;
13780
+ process.env[key] = parts.join("=").trim().replace(/^['"]|['"]$/g, "");
13250
13781
  }
13251
- const branch = strategy.name ?? `sandblocks/${randomUUID2().slice(0, 8)}`;
13252
- const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
13253
- await mkdir4(dirname(worktree), { recursive: true });
13254
- await checked(["git", "-C", hostCwd, "worktree", "add", "-B", branch, worktree, "HEAD"], timeoutMs);
13782
+ }
13783
+ async function applyManagedRoutingPolicy(context, stack, preview) {
13784
+ const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
13785
+ const policy = body.policy;
13786
+ const primaryDomains = Object.fromEntries(Object.entries(policy.domains ?? {}).flatMap(([service, domains]) => {
13787
+ const domain = Array.isArray(domains) ? domains[0] : domains;
13788
+ return domain ? [[service, domain]] : [];
13789
+ }));
13255
13790
  return {
13256
- hostCwd,
13257
- runtimeCwd: worktree,
13258
- branch,
13259
- worktree,
13260
- mergeToHead: strategy.type === "merge-to-head",
13261
- originalHead
13791
+ ...preview,
13792
+ baseDomain: policy.baseDomain ?? preview.baseDomain,
13793
+ domains: { ...preview.domains, ...primaryDomains },
13794
+ stableDomains: { ...preview.stableDomains, ...policy.stableDomains }
13262
13795
  };
13263
13796
  }
13264
- async function mergeBranch(git3, timeoutMs = 60000) {
13265
- if (!git3.branch)
13266
- throw new Error("merge branch is unavailable");
13267
- await checked(["git", "-C", git3.hostCwd, "merge", "--no-edit", git3.branch], timeoutMs);
13268
- }
13269
- async function gitHead(cwd) {
13270
- const result = await executeProcess(["git", "-C", cwd, "rev-parse", "HEAD"]);
13271
- return result.exitCode === 0 ? result.stdout.trim() : "";
13272
- }
13273
- async function collectCommits(cwd, baseline, timeoutMs = 30000) {
13274
- if (!baseline)
13275
- return [];
13276
- const result = await executeProcess(["git", "-C", cwd, "log", "--format=%H%x00%s", `${baseline}..HEAD`], {
13277
- timeoutMs
13278
- });
13279
- if (result.exitCode !== 0 || !result.stdout.trim())
13280
- return [];
13281
- return result.stdout.trim().split(`
13282
- `).map((line) => {
13283
- const [sha, message] = line.split("\x00");
13284
- if (!sha)
13285
- throw new Error("Git returned an invalid commit record");
13286
- return { sha, ...message ? { message } : {} };
13287
- });
13797
+ function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
13798
+ return Object.fromEntries(preview.services.flatMap((service) => {
13799
+ if (service.kind === "process" && !service.port)
13800
+ return [];
13801
+ const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
13802
+ const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
13803
+ const configuredStableHosts = preview.stableDomains[service.id];
13804
+ const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.deploymentType}-${stack}-${service.id}.${preview.baseDomain}`;
13805
+ return [
13806
+ [`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
13807
+ [`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
13808
+ ];
13809
+ }));
13288
13810
  }
13289
- async function checked(command, timeoutMs) {
13290
- const result = await executeProcess(command, { timeoutMs });
13291
- if (result.exitCode !== 0)
13292
- throw new Error(`${command[0]} failed: ${result.stderr || result.stdout}`);
13811
+ async function managedServiceDomains(context, stack, preview, deploymentId) {
13812
+ const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
13813
+ const domains = body.policy.domains ?? {};
13814
+ return Object.keys(domains).length ? domains : resolvedServiceDomains(context.state.sandboxId, deploymentId, preview);
13293
13815
  }
13294
- async function runHostHooks(hooks, cwd) {
13295
- for (const hook of hooks ?? []) {
13296
- const command = hookCommand(hook);
13297
- const result = await executeProcess(command, {
13298
- cwd: resolve(cwd, hook.cwd ?? "."),
13299
- env: hook.env,
13300
- timeoutMs: hook.timeoutMs
13301
- });
13302
- if (result.exitCode !== 0)
13303
- throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
13304
- }
13816
+ function resolvedServiceDomains(sandboxId, deploymentId, preview) {
13817
+ return Object.fromEntries(preview.services.map((service) => [
13818
+ service.id,
13819
+ resolvedServiceDomain(sandboxId, deploymentId, service.id, preview)
13820
+ ]));
13305
13821
  }
13306
- async function runSandboxHooks(runtime, hooks) {
13307
- for (const hook of hooks ?? []) {
13308
- const result = await runtime.exec(hookCommand(hook), {
13309
- cwd: hook.cwd,
13310
- env: hook.env,
13311
- timeoutMs: hook.timeoutMs
13312
- });
13313
- if (result.exitCode !== 0)
13314
- throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
13315
- }
13822
+ 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)}`;
13824
+ const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
13825
+ return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
13316
13826
  }
13317
- function hookCommand(hook) {
13318
- if (Boolean(hook.command) === Boolean(hook.shell))
13319
- throw new Error("hook requires exactly one of command or shell");
13320
- if (hook.command)
13321
- return hook.command;
13322
- if (!hook.shell)
13323
- throw new Error("hook shell command is required");
13324
- return ["sh", "-lc", hook.shell];
13827
+ function serviceMemoryReservation(services) {
13828
+ return services.reduce((total, service) => {
13829
+ const match = /^(\d+)(m|g)$/i.exec(service.memory);
13830
+ if (!match)
13831
+ return total;
13832
+ const amount = Number(match[1]);
13833
+ return total + amount * (match[2]?.toLowerCase() === "g" ? 1024 ** 3 : 1024 ** 2);
13834
+ }, 0);
13325
13835
  }
13326
- function createLogger(options, cwd) {
13327
- const config = options ?? { type: "file" };
13328
- const file = resolve(cwd, config.path ?? `.sandblocks/logs/${Date.now()}.log`);
13329
- return async (event) => {
13330
- const line = `${JSON.stringify(event)}
13331
- `;
13332
- if (config.type === "stdout")
13333
- process.stdout.write(event.type === "text" ? event.text : event.type === "raw" ? `${event.line}
13334
- ` : line);
13335
- if (config.type !== "stdout" && config.type !== "silent") {
13336
- await mkdir4(dirname(file), { recursive: true });
13337
- await appendFile(file, line);
13338
- }
13339
- try {
13340
- await config.onAgentStreamEvent?.(event);
13341
- } catch {}
13342
- };
13836
+ function statePath(root, environment) {
13837
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(environment))
13838
+ throw new Error("sandbox environment is invalid");
13839
+ return path5.join(root, ".sandblocks", `sandbox-${environment}.json`);
13343
13840
  }
13344
- async function withTimeout(promise, timeoutMs, label) {
13345
- let timer;
13346
- try {
13347
- return await Promise.race([
13348
- promise,
13349
- new Promise((_, reject) => {
13350
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
13351
- })
13352
- ]);
13353
- } finally {
13354
- if (timer)
13355
- clearTimeout(timer);
13356
- }
13841
+ async function writeState(file, state) {
13842
+ await mkdir6(path5.dirname(file), { recursive: true, mode: 448 });
13843
+ await chmod3(path5.dirname(file), 448);
13844
+ const temporary = `${file}.${process.pid}.tmp`;
13845
+ await Bun.write(temporary, `${JSON.stringify(state, null, 2)}
13846
+ `, { mode: 384 });
13847
+ await chmod3(temporary, 384);
13848
+ await rename(temporary, file);
13357
13849
  }
13358
- function assertProviderCapabilities(provider, options) {
13359
- const capabilities = provider.capabilities;
13360
- if (options.mounts?.length && !capabilities.bindMounts)
13361
- throw new Error(`${provider.kind} provider does not support bind mounts`);
13362
- if (options.network && !capabilities.networks)
13363
- throw new Error(`${provider.kind} provider does not support network selection`);
13364
- if (options.devices?.length && !capabilities.devices)
13365
- throw new Error(`${provider.kind} provider does not support devices`);
13366
- if ((options.retention === "always" || options.retention === "on-failure") && !capabilities.persistent)
13367
- throw new Error(`${provider.kind} provider does not support retained sandboxes`);
13850
+ function redactedState(state) {
13851
+ return { ...state, leaseToken: "<redacted>" };
13368
13852
  }
13369
- function array(value) {
13370
- return Array.isArray(value) ? value : [value];
13853
+ function printOutput(operation) {
13854
+ if (operation.result?.stdout)
13855
+ process.stdout.write(String(operation.result.stdout));
13856
+ if (operation.result?.stderr)
13857
+ process.stderr.write(String(operation.result.stderr));
13858
+ if (Number(operation.result?.exitCode ?? 0) !== 0)
13859
+ throw new Error(`command exited ${operation.result.exitCode}`);
13371
13860
  }
13372
- async function initSdk(options = {}) {
13373
- const cwd = resolve2(options.cwd ?? process.cwd());
13374
- const root = resolve2(cwd, ".sandblocks");
13375
- const files = {
13376
- "main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
13377
- import { DockerProvider } from "@sandblocks/sdk/providers/docker";
13378
-
13379
- await using sandbox = await createSandbox({
13380
- provider: new DockerProvider({ image: "node:22-bookworm" }),
13381
- cwd: process.cwd(),
13382
- });
13383
-
13384
- const result = await sandbox.run({
13385
- agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
13386
- promptFile: ".sandblocks/prompts/implement.md",
13387
- logging: { type: "stdout" },
13388
- });
13389
-
13390
- console.log(result);
13391
- `,
13392
- "prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
13393
13861
 
13394
- <promise>COMPLETE</promise>
13395
- `,
13396
- ".env.example": `# Prefer Sandblocks managed secrets for remote providers.
13397
- CLAUDE_CODE_OAUTH_TOKEN=
13398
- # ANTHROPIC_API_KEY=
13399
- SANDBLOCKS_API_URL=
13400
- SANDBLOCKS_API_KEY=
13401
- `,
13402
- "config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
13403
- `
13404
- };
13405
- const written = [];
13406
- for (const [relative, content] of Object.entries(files)) {
13407
- const path6 = resolve2(root, relative);
13408
- await mkdir22(resolve2(path6, ".."), { recursive: true });
13409
- if (!options.force && await access(path6).then(() => true).catch(() => false))
13410
- continue;
13411
- await writeFile2(path6, content, { mode: relative.includes(".env") ? 384 : 420 });
13412
- written.push(path6);
13413
- }
13414
- return written;
13415
- }
13862
+ // packages/libs/cli/src/sdk.ts
13863
+ import { mkdir as mkdir7, readFile as readFile7, rm as rm6, writeFile as writeFile4 } from "fs/promises";
13864
+ import { dirname as dirname2, resolve as resolve7 } from "path";
13416
13865
 
13417
- // ../sdk/dist/providers/docker.js
13866
+ // packages/libs/sdk/dist/providers/docker.js
13418
13867
  import { realpath as realpath3 } from "fs/promises";
13419
13868
  import { resolve as resolve3 } from "path";
13420
- import { spawn as spawn2 } from "child_process";
13869
+ import { spawn as spawn3 } from "child_process";
13421
13870
  async function executeProcess2(command, options = {}) {
13422
13871
  if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
13423
13872
  throw new Error("command must contain safe non-empty arguments");
@@ -13426,7 +13875,7 @@ async function executeProcess2(command, options = {}) {
13426
13875
  const [executable, ...args] = command;
13427
13876
  if (!executable)
13428
13877
  throw new Error("command executable is required");
13429
- const child = spawn2(executable, args, {
13878
+ const child = spawn3(executable, args, {
13430
13879
  cwd: options.cwd ?? options.hostCwd,
13431
13880
  env: options.env ? { ...process.env, ...options.env } : process.env,
13432
13881
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
@@ -13660,10 +14109,10 @@ class DockerProvider extends OciProvider {
13660
14109
  }
13661
14110
  }
13662
14111
 
13663
- // ../sdk/dist/providers/podman.js
14112
+ // packages/libs/sdk/dist/providers/podman.js
13664
14113
  import { realpath as realpath4 } from "fs/promises";
13665
14114
  import { resolve as resolve5 } from "path";
13666
- import { spawn as spawn3 } from "child_process";
14115
+ import { spawn as spawn4 } from "child_process";
13667
14116
  async function executeProcess3(command, options = {}) {
13668
14117
  if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
13669
14118
  throw new Error("command must contain safe non-empty arguments");
@@ -13672,7 +14121,7 @@ async function executeProcess3(command, options = {}) {
13672
14121
  const [executable, ...args] = command;
13673
14122
  if (!executable)
13674
14123
  throw new Error("command executable is required");
13675
- const child = spawn3(executable, args, {
14124
+ const child = spawn4(executable, args, {
13676
14125
  cwd: options.cwd ?? options.hostCwd,
13677
14126
  env: options.env ? { ...process.env, ...options.env } : process.env,
13678
14127
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
@@ -13906,12 +14355,12 @@ class PodmanProvider extends OciProvider2 {
13906
14355
  }
13907
14356
  }
13908
14357
 
13909
- // ../sdk/dist/providers/remote.js
14358
+ // packages/libs/sdk/dist/providers/remote.js
13910
14359
  import { randomUUID as randomUUID3 } from "crypto";
13911
14360
  import { mkdtemp, readFile as readFile6, rm as rm5, stat as stat2, writeFile as writeFile3 } from "fs/promises";
13912
14361
  import { tmpdir } from "os";
13913
14362
  import { join } from "path";
13914
- import { spawn as spawn4 } from "child_process";
14363
+ import { spawn as spawn5 } from "child_process";
13915
14364
  async function executeProcess4(command, options = {}) {
13916
14365
  if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
13917
14366
  throw new Error("command must contain safe non-empty arguments");
@@ -13920,7 +14369,7 @@ async function executeProcess4(command, options = {}) {
13920
14369
  const [executable, ...args] = command;
13921
14370
  if (!executable)
13922
14371
  throw new Error("command executable is required");
13923
- const child = spawn4(executable, args, {
14372
+ const child = spawn5(executable, args, {
13924
14373
  cwd: options.cwd ?? options.hostCwd,
13925
14374
  env: options.env ? { ...process.env, ...options.env } : process.env,
13926
14375
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
@@ -14230,10 +14679,10 @@ async function sourceBundle(cwd) {
14230
14679
  }
14231
14680
  }
14232
14681
 
14233
- // ../sdk/dist/providers/unsafe-host.js
14682
+ // packages/libs/sdk/dist/providers/unsafe-host.js
14234
14683
  import { cp as cp2, realpath as realpath5 } from "fs/promises";
14235
14684
  import { resolve as resolve6 } from "path";
14236
- import { spawn as spawn5 } from "child_process";
14685
+ import { spawn as spawn6 } from "child_process";
14237
14686
  async function executeProcess5(command, options = {}) {
14238
14687
  if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
14239
14688
  throw new Error("command must contain safe non-empty arguments");
@@ -14242,7 +14691,7 @@ async function executeProcess5(command, options = {}) {
14242
14691
  const [executable, ...args] = command;
14243
14692
  if (!executable)
14244
14693
  throw new Error("command executable is required");
14245
- const child = spawn5(executable, args, {
14694
+ const child = spawn6(executable, args, {
14246
14695
  cwd: options.cwd ?? options.hostCwd,
14247
14696
  env: options.env ? { ...process.env, ...options.env } : process.env,
14248
14697
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
@@ -14339,7 +14788,7 @@ class UnsafeHostProvider {
14339
14788
  }
14340
14789
  }
14341
14790
 
14342
- // src/sdk.ts
14791
+ // packages/libs/cli/src/sdk.ts
14343
14792
  async function runSdkCommand(args, helpers) {
14344
14793
  const [command = "help", ...rest] = args;
14345
14794
  const parsed = helpers.parse(rest);
@@ -14406,7 +14855,7 @@ async function sdkSandbox(args, helpers, defaultCwd) {
14406
14855
  hooks: hooksFrom(options),
14407
14856
  copyToWorktree: multi(options["copy-to-worktree"])
14408
14857
  });
14409
- await mkdir5(dirname2(stateFile), { recursive: true });
14858
+ await mkdir7(dirname2(stateFile), { recursive: true });
14410
14859
  const strategy = branchFrom(options, helpers);
14411
14860
  const worktree = strategy.type === "worktree" || strategy.type === "merge-to-head" ? sandbox2.cwd : undefined;
14412
14861
  await writeFile4(stateFile, JSON.stringify({
@@ -14653,14 +15102,14 @@ function parseMount(value) {
14653
15102
  return { source, target, readonly: mode === "ro" };
14654
15103
  }
14655
15104
 
14656
- // src/cli.ts
15105
+ // packages/libs/cli/src/cli.ts
14657
15106
  var HELP = `Sandblocks CLI
14658
15107
 
14659
15108
  sandblocks configure [directory] [--name <stack>] [--force]
14660
15109
  sandblocks validate [directory] [--config <path>] [--json]
14661
15110
  sandblocks register [directory] --project <id> [--repository-url <url>]
14662
15111
  [--api-url <url>] [--api-key <key>] [--json]
14663
- sandblocks doctor [directory] [--api-url <url>]
15112
+ sandblocks doctor [directory] [--api-url <url>] [--explain-placement] [--project <id>] [--json]
14664
15113
  sandblocks whoami [directory] [--api-url <url>] [--api-key <key>] [--json]
14665
15114
  sandblocks hooks <install|status|uninstall> [directory]
14666
15115
  sandblocks hooks run post-commit [directory]
@@ -14670,10 +15119,12 @@ var HELP = `Sandblocks CLI
14670
15119
  sandblocks app <list|get|settings|settings-set> --project <id> [--stack <id>] [--app <id>] [--file <json>] [--json]
14671
15120
  sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
14672
15121
  [--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
14673
- sandblocks sandbox up [directory] [--environment <id>] [--project <id>]
15122
+ sandblocks artifact push <directory> --project <id> --stack <id> [--pool <pool>] [--json]
15123
+ sandblocks sandbox up [directory] [--environment <id>] [--project <id>] [--build-pool <pool>]
14674
15124
  sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
14675
- sandblocks sandbox deploy [directory] [--environment <id>]
14676
- sandblocks sandbox redeploy [directory] [--environment <id>]
15125
+ sandblocks sandbox deploy [directory] [--environment <id>] [--build-pool <pool>]
15126
+ [--artifact <image@sha256:digest>] [--deployment-id <uuid>] [--resume]
15127
+ sandblocks sandbox redeploy [directory] [--environment <id>] [--build-pool <pool>]
14677
15128
  sandblocks sandbox status [directory] [--environment <id>] [--json]
14678
15129
  sandblocks sandbox promote --project <id> --sandbox <id> [--json]
14679
15130
  sandblocks sandbox agent <run|list|get|cancel|redeploy> --project <id> --sandbox <id>
@@ -14708,7 +15159,7 @@ Low-level deploy remains available with --workspace, --sandbox, --host,
14708
15159
  Authentication: --api-key or SANDBLOCKS_API_KEY
14709
15160
  API endpoint: --api-url or SANDBLOCKS_API_URL
14710
15161
  `;
14711
- async function run2(argv = process.argv.slice(2)) {
15162
+ async function run3(argv = process.argv.slice(2)) {
14712
15163
  await loadCliEnvironment(process.cwd());
14713
15164
  const [command = "help", ...args] = argv;
14714
15165
  if (command === "configure" || command === "init")
@@ -14740,6 +15191,8 @@ async function run2(argv = process.argv.slice(2)) {
14740
15191
  await resource(command, args);
14741
15192
  else if (command === "secret")
14742
15193
  await secret(args);
15194
+ else if (command === "artifact")
15195
+ await artifact(args);
14743
15196
  else if (command === "sandbox")
14744
15197
  await sandbox(args);
14745
15198
  else if (command === "sdk")
@@ -14840,6 +15293,38 @@ async function doctor(args) {
14840
15293
  if (!response.ok)
14841
15294
  throw new Error(`API health failed (${response.status})`);
14842
15295
  console.log(`api ok (${apiUrl})`);
15296
+ if (options["explain-placement"]) {
15297
+ const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
15298
+ const projectId = option2(options, "project") ?? process.env.SANDBLOCKS_PROJECT_ID;
15299
+ if (!apiKey || !projectId)
15300
+ throw new Error("doctor --explain-placement requires a project and Sandblocks API key");
15301
+ const checks = [
15302
+ { label: "source import", kind: "workspace.import", role: "build", memory: 1024 ** 3 },
15303
+ { label: "artifact publish", kind: "workspace.artifact.publish", role: "build", memory: 1024 ** 3 },
15304
+ { label: "service deploy", kind: "workspace.service.deploy", role: "runtime", memory: 6 * 1024 ** 3 }
15305
+ ];
15306
+ const explanations = await Promise.all(checks.map(async (check) => {
15307
+ const query = new URLSearchParams({
15308
+ kind: check.kind,
15309
+ role: check.role,
15310
+ memoryReservationBytes: String(check.memory)
15311
+ });
15312
+ const result = await sandblocksRequest(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/placement/explain?${query}`);
15313
+ return { ...check, explanation: result.explanation };
15314
+ }));
15315
+ if (options.json)
15316
+ console.log(JSON.stringify({ projectId, explanations }, null, 2));
15317
+ else
15318
+ for (const check of explanations) {
15319
+ const explanation = objectRecord(check.explanation);
15320
+ console.log(`${check.label.padEnd(16)} ${explanation.eligible ? "eligible" : "blocked"}`);
15321
+ if (!explanation.eligible && Array.isArray(explanation.candidates))
15322
+ for (const candidate of explanation.candidates) {
15323
+ const row = objectRecord(candidate);
15324
+ console.log(` ${row.hostName ?? "unknown"}/${row.workerName ?? "unknown"}: ${row.reasons?.join(", ") || "rejected"}`);
15325
+ }
15326
+ }
15327
+ }
14843
15328
  }
14844
15329
  const gitDetected = await stat3(path6.join(root, ".git")).then(() => true).catch(() => false);
14845
15330
  console.log(`git ${gitDetected ? "ok" : "not detected"}`);
@@ -14955,6 +15440,45 @@ async function secret(args) {
14955
15440
  }
14956
15441
  }
14957
15442
  }
15443
+ async function artifact(args) {
15444
+ const [action = "push", ...rest] = args;
15445
+ if (action !== "push")
15446
+ throw new Error(`unsupported artifact command '${action}'`);
15447
+ const options = parse2(rest);
15448
+ const root = path6.resolve(options.positionals[0] ?? process.cwd());
15449
+ const projectId = option2(options, "project") ?? process.env.SANDBLOCKS_PROJECT_ID;
15450
+ const stack = option2(options, "stack");
15451
+ const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
15452
+ const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
15453
+ if (!projectId || !stack)
15454
+ throw new Error("artifact push requires --project and --stack");
15455
+ if (!apiUrl || !apiKey)
15456
+ throw new Error("artifact push requires Sandblocks API URL and key");
15457
+ const deploymentId = option2(options, "deployment-id") ?? randomUUID4();
15458
+ if (!/^[a-f0-9-]{36}$/.test(deploymentId))
15459
+ throw new Error("artifact deployment ID must be a UUID");
15460
+ const workspaceId = option2(options, "workspace") ?? `prebuilt-${deploymentId.slice(0, 8)}`;
15461
+ const bundle = await createPrebuiltTar(root);
15462
+ const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
15463
+ const result = await client2.uploadPrebuiltArtifact({
15464
+ projectId,
15465
+ workspaceId,
15466
+ deploymentId,
15467
+ stack,
15468
+ bundle,
15469
+ pool: option2(options, "pool") ?? "build-fleet",
15470
+ workspaceImage: option2(options, "image")
15471
+ });
15472
+ if (options.json)
15473
+ console.log(JSON.stringify(result, null, 2));
15474
+ else {
15475
+ console.log(`artifact ${result.image}`);
15476
+ console.log(`deployment ${result.deploymentId}`);
15477
+ console.log(`workspace ${result.workspaceId}`);
15478
+ console.log(`bytes ${result.bytes}`);
15479
+ console.log(`deploy with: sandblocks sandbox deploy . --artifact ${result.image} --deployment-id ${result.deploymentId}`);
15480
+ }
15481
+ }
14958
15482
  function printResources(kind, action, body) {
14959
15483
  if (action === "settings") {
14960
15484
  console.log(JSON.stringify(body.settings ?? {}, null, 2));
@@ -15124,6 +15648,51 @@ async function sandboxPromote(args) {
15124
15648
  console.log(`promoted ${sandboxId} to production`);
15125
15649
  }
15126
15650
  var sourceFileCount = 0;
15651
+ async function createPrebuiltTar(root) {
15652
+ const names = [];
15653
+ const visit2 = async (directory, prefix = "") => {
15654
+ for (const entry of await readdir2(directory, { withFileTypes: true })) {
15655
+ if (!prefix && [".git", ".sandblocks"].includes(entry.name))
15656
+ continue;
15657
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
15658
+ const absolute = path6.join(directory, entry.name);
15659
+ if (entry.isSymbolicLink())
15660
+ throw new Error(`prebuilt artifact symlinks are not supported: ${relative}`);
15661
+ if (entry.isDirectory())
15662
+ await visit2(absolute, relative);
15663
+ else if (entry.isFile())
15664
+ names.push(relative);
15665
+ if (names.length > 20000)
15666
+ throw new Error("prebuilt artifact contains more than 20000 files");
15667
+ }
15668
+ };
15669
+ await visit2(root);
15670
+ if (!names.length)
15671
+ throw new Error("prebuilt artifact directory has no files");
15672
+ const chunks = [];
15673
+ let total = 1024;
15674
+ for (const name of names.sort()) {
15675
+ if (name.includes("\\") || name.startsWith("/") || name.split("/").includes(".."))
15676
+ throw new Error(`prebuilt artifact path is unsafe: ${name}`);
15677
+ const file = path6.join(root, name);
15678
+ const info = await lstat(file);
15679
+ const contents = new Uint8Array(await readFile8(file));
15680
+ const header = tarHeader(name, contents.byteLength, info.mode, Math.floor(info.mtimeMs / 1000));
15681
+ const padding = (512 - contents.byteLength % 512) % 512;
15682
+ total += 512 + contents.byteLength + padding;
15683
+ if (total > 256 * 1024 * 1024)
15684
+ throw new Error("prebuilt artifact exceeds 256 MiB");
15685
+ chunks.push(header, contents, new Uint8Array(padding));
15686
+ }
15687
+ chunks.push(new Uint8Array(1024));
15688
+ const output = new Uint8Array(total);
15689
+ let offset = 0;
15690
+ for (const chunk of chunks) {
15691
+ output.set(chunk, offset);
15692
+ offset += chunk.byteLength;
15693
+ }
15694
+ return output;
15695
+ }
15127
15696
  async function createSourceTar(root) {
15128
15697
  const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
15129
15698
  cwd: root,
@@ -15423,15 +15992,15 @@ async function loadCliEnvironment(root) {
15423
15992
  }
15424
15993
  if (import.meta.main) {
15425
15994
  try {
15426
- await run2();
15995
+ await run3();
15427
15996
  } catch (error) {
15428
15997
  console.error(`sandblocks: ${error instanceof Error ? error.message : String(error)}`);
15429
15998
  process.exitCode = 1;
15430
15999
  }
15431
16000
  }
15432
16001
  export {
15433
- run2 as run,
16002
+ run3 as run,
15434
16003
  parse2 as parse
15435
16004
  };
15436
16005
 
15437
- //# debugId=EB337A76C8A48F0564756E2164756E21
16006
+ //# debugId=E7F4E37BF2F49E8E64756E2164756E21