@sandblocks/cli 0.5.0-b.5 → 0.5.0-b.7

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,
@@ -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
  }
@@ -4280,7 +4280,7 @@ var init_schema = __esm(() => {
4280
4280
  SandblocksManifestV2Schema = CompleteSandblocksManifestV2Schema.superRefine(validateReferences);
4281
4281
  });
4282
4282
 
4283
- // ../manifest/src/resolve.ts
4283
+ // packages/libs/manifest/src/resolve.ts
4284
4284
  function resolveProjectGraph(manifest) {
4285
4285
  return compileProjectGraph(manifest);
4286
4286
  }
@@ -4381,9 +4381,16 @@ function executionService(service) {
4381
4381
  }
4382
4382
  var init_resolve = () => {};
4383
4383
 
4384
- // ../manifest/src/images.ts
4384
+ // packages/libs/manifest/src/images.ts
4385
4385
  import { readFile, realpath, stat } from "fs/promises";
4386
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
+ }
4387
4394
  async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".") {
4388
4395
  const root = await realpath(repositoryRoot);
4389
4396
  const workspace = await containedDirectory(root, workspaceRoot);
@@ -4414,7 +4421,7 @@ async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".")
4414
4421
  return selection("node", "default Node.js environment");
4415
4422
  }
4416
4423
  function selection(family, evidence) {
4417
- return { family, image: SANDBLOCKS_PREBUILT_IMAGES[family], evidence };
4424
+ return { family, image: resolvePrebuiltImage(family), evidence };
4418
4425
  }
4419
4426
  async function containedDirectory(repositoryRoot, relative) {
4420
4427
  const candidate = path.resolve(repositoryRoot, relative);
@@ -4480,14 +4487,14 @@ function relativeEvidence(repositoryRoot, directory, file) {
4480
4487
  var SANDBLOCKS_PREBUILT_IMAGES, LOCKFILES;
4481
4488
  var init_images = __esm(() => {
4482
4489
  SANDBLOCKS_PREBUILT_IMAGES = {
4483
- bun: "ghcr.io/honeycluster/sandblocks-executor-bun:1.0.0",
4484
- pnpm: "ghcr.io/honeycluster/sandblocks-executor-pnpm:1.0.0",
4485
- npm: "ghcr.io/honeycluster/sandblocks-executor-npm:1.0.0",
4486
- yarn: "ghcr.io/honeycluster/sandblocks-executor-yarn:1.0.0",
4487
- corepack: "ghcr.io/honeycluster/sandblocks-executor-corepack:1.0.0",
4488
- node: "ghcr.io/honeycluster/sandblocks-executor-node:1.0.0",
4489
- "node-native": "ghcr.io/honeycluster/sandblocks-executor-node-native:1.0.0",
4490
- 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"
4491
4498
  };
4492
4499
  LOCKFILES = [
4493
4500
  { file: "bun.lock", family: "bun" },
@@ -4499,7 +4506,7 @@ var init_images = __esm(() => {
4499
4506
  ];
4500
4507
  });
4501
4508
 
4502
- // ../../../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
4503
4510
  var require_identity = __commonJS((exports) => {
4504
4511
  var ALIAS = Symbol.for("yaml.alias");
4505
4512
  var DOC = Symbol.for("yaml.document");
@@ -4553,7 +4560,7 @@ var require_identity = __commonJS((exports) => {
4553
4560
  exports.isSeq = isSeq;
4554
4561
  });
4555
4562
 
4556
- // ../../../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
4557
4564
  var require_visit = __commonJS((exports) => {
4558
4565
  var identity = require_identity();
4559
4566
  var BREAK = Symbol("break visit");
@@ -4708,7 +4715,7 @@ var require_visit = __commonJS((exports) => {
4708
4715
  exports.visitAsync = visitAsync;
4709
4716
  });
4710
4717
 
4711
- // ../../../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
4712
4719
  var require_directives = __commonJS((exports) => {
4713
4720
  var identity = require_identity();
4714
4721
  var visit = require_visit();
@@ -4860,7 +4867,7 @@ var require_directives = __commonJS((exports) => {
4860
4867
  exports.Directives = Directives;
4861
4868
  });
4862
4869
 
4863
- // ../../../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
4864
4871
  var require_anchors = __commonJS((exports) => {
4865
4872
  var identity = require_identity();
4866
4873
  var visit = require_visit();
@@ -4922,7 +4929,7 @@ var require_anchors = __commonJS((exports) => {
4922
4929
  exports.findNewAnchor = findNewAnchor;
4923
4930
  });
4924
4931
 
4925
- // ../../../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
4926
4933
  var require_applyReviver = __commonJS((exports) => {
4927
4934
  function applyReviver(reviver, obj, key, val) {
4928
4935
  if (val && typeof val === "object") {
@@ -4969,7 +4976,7 @@ var require_applyReviver = __commonJS((exports) => {
4969
4976
  exports.applyReviver = applyReviver;
4970
4977
  });
4971
4978
 
4972
- // ../../../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
4973
4980
  var require_toJS = __commonJS((exports) => {
4974
4981
  var identity = require_identity();
4975
4982
  function toJS(value, arg, ctx) {
@@ -4996,7 +5003,7 @@ var require_toJS = __commonJS((exports) => {
4996
5003
  exports.toJS = toJS;
4997
5004
  });
4998
5005
 
4999
- // ../../../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
5000
5007
  var require_Node = __commonJS((exports) => {
5001
5008
  var applyReviver = require_applyReviver();
5002
5009
  var identity = require_identity();
@@ -5033,7 +5040,7 @@ var require_Node = __commonJS((exports) => {
5033
5040
  exports.NodeBase = NodeBase;
5034
5041
  });
5035
5042
 
5036
- // ../../../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
5037
5044
  var require_Alias = __commonJS((exports) => {
5038
5045
  var anchors = require_anchors();
5039
5046
  var visit = require_visit();
@@ -5143,7 +5150,7 @@ var require_Alias = __commonJS((exports) => {
5143
5150
  exports.Alias = Alias;
5144
5151
  });
5145
5152
 
5146
- // ../../../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
5147
5154
  var require_Scalar = __commonJS((exports) => {
5148
5155
  var identity = require_identity();
5149
5156
  var Node = require_Node();
@@ -5171,7 +5178,7 @@ var require_Scalar = __commonJS((exports) => {
5171
5178
  exports.isScalarValue = isScalarValue;
5172
5179
  });
5173
5180
 
5174
- // ../../../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
5175
5182
  var require_createNode = __commonJS((exports) => {
5176
5183
  var Alias = require_Alias();
5177
5184
  var identity = require_identity();
@@ -5243,7 +5250,7 @@ var require_createNode = __commonJS((exports) => {
5243
5250
  exports.createNode = createNode;
5244
5251
  });
5245
5252
 
5246
- // ../../../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
5247
5254
  var require_Collection = __commonJS((exports) => {
5248
5255
  var createNode = require_createNode();
5249
5256
  var identity = require_identity();
@@ -5358,7 +5365,7 @@ var require_Collection = __commonJS((exports) => {
5358
5365
  exports.isEmptyPath = isEmptyPath;
5359
5366
  });
5360
5367
 
5361
- // ../../../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
5362
5369
  var require_stringifyComment = __commonJS((exports) => {
5363
5370
  var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#");
5364
5371
  function indentComment(comment, indent) {
@@ -5375,7 +5382,7 @@ var require_stringifyComment = __commonJS((exports) => {
5375
5382
  exports.stringifyComment = stringifyComment;
5376
5383
  });
5377
5384
 
5378
- // ../../../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
5379
5386
  var require_foldFlowLines = __commonJS((exports) => {
5380
5387
  var FOLD_FLOW = "flow";
5381
5388
  var FOLD_BLOCK = "block";
@@ -5512,7 +5519,7 @@ ${indent}${text.slice(fold + 1, end2)}`;
5512
5519
  exports.foldFlowLines = foldFlowLines;
5513
5520
  });
5514
5521
 
5515
- // ../../../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
5516
5523
  var require_stringifyString = __commonJS((exports) => {
5517
5524
  var Scalar = require_Scalar();
5518
5525
  var foldFlowLines = require_foldFlowLines();
@@ -5810,7 +5817,7 @@ ${indent}`);
5810
5817
  exports.stringifyString = stringifyString;
5811
5818
  });
5812
5819
 
5813
- // ../../../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
5814
5821
  var require_stringify = __commonJS((exports) => {
5815
5822
  var anchors = require_anchors();
5816
5823
  var identity = require_identity();
@@ -5931,7 +5938,7 @@ ${ctx.indent}${str}`;
5931
5938
  exports.stringify = stringify;
5932
5939
  });
5933
5940
 
5934
- // ../../../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
5935
5942
  var require_stringifyPair = __commonJS((exports) => {
5936
5943
  var identity = require_identity();
5937
5944
  var Scalar = require_Scalar();
@@ -6067,7 +6074,7 @@ ${ctx.indent}`;
6067
6074
  exports.stringifyPair = stringifyPair;
6068
6075
  });
6069
6076
 
6070
- // ../../../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
6071
6078
  var require_log = __commonJS((exports) => {
6072
6079
  var node_process = __require("process");
6073
6080
  function debug(logLevel, ...messages) {
@@ -6086,7 +6093,7 @@ var require_log = __commonJS((exports) => {
6086
6093
  exports.warn = warn;
6087
6094
  });
6088
6095
 
6089
- // ../../../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
6090
6097
  var require_merge = __commonJS((exports) => {
6091
6098
  var identity = require_identity();
6092
6099
  var Scalar = require_Scalar();
@@ -6143,7 +6150,7 @@ var require_merge = __commonJS((exports) => {
6143
6150
  exports.merge = merge;
6144
6151
  });
6145
6152
 
6146
- // ../../../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
6147
6154
  var require_addPairToJSMap = __commonJS((exports) => {
6148
6155
  var log = require_log();
6149
6156
  var merge = require_merge();
@@ -6204,7 +6211,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
6204
6211
  exports.addPairToJSMap = addPairToJSMap;
6205
6212
  });
6206
6213
 
6207
- // ../../../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
6208
6215
  var require_Pair = __commonJS((exports) => {
6209
6216
  var createNode = require_createNode();
6210
6217
  var stringifyPair = require_stringifyPair();
@@ -6242,7 +6249,7 @@ var require_Pair = __commonJS((exports) => {
6242
6249
  exports.createPair = createPair;
6243
6250
  });
6244
6251
 
6245
- // ../../../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
6246
6253
  var require_stringifyCollection = __commonJS((exports) => {
6247
6254
  var identity = require_identity();
6248
6255
  var stringify = require_stringify();
@@ -6394,7 +6401,7 @@ ${indent}${end}`;
6394
6401
  exports.stringifyCollection = stringifyCollection;
6395
6402
  });
6396
6403
 
6397
- // ../../../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
6398
6405
  var require_YAMLMap = __commonJS((exports) => {
6399
6406
  var stringifyCollection = require_stringifyCollection();
6400
6407
  var addPairToJSMap = require_addPairToJSMap();
@@ -6521,7 +6528,7 @@ var require_YAMLMap = __commonJS((exports) => {
6521
6528
  exports.findPair = findPair;
6522
6529
  });
6523
6530
 
6524
- // ../../../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
6525
6532
  var require_map = __commonJS((exports) => {
6526
6533
  var identity = require_identity();
6527
6534
  var YAMLMap = require_YAMLMap();
@@ -6540,7 +6547,7 @@ var require_map = __commonJS((exports) => {
6540
6547
  exports.map = map;
6541
6548
  });
6542
6549
 
6543
- // ../../../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
6544
6551
  var require_YAMLSeq = __commonJS((exports) => {
6545
6552
  var createNode = require_createNode();
6546
6553
  var stringifyCollection = require_stringifyCollection();
@@ -6633,7 +6640,7 @@ var require_YAMLSeq = __commonJS((exports) => {
6633
6640
  exports.YAMLSeq = YAMLSeq;
6634
6641
  });
6635
6642
 
6636
- // ../../../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
6637
6644
  var require_seq = __commonJS((exports) => {
6638
6645
  var identity = require_identity();
6639
6646
  var YAMLSeq = require_YAMLSeq();
@@ -6652,7 +6659,7 @@ var require_seq = __commonJS((exports) => {
6652
6659
  exports.seq = seq;
6653
6660
  });
6654
6661
 
6655
- // ../../../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
6656
6663
  var require_string = __commonJS((exports) => {
6657
6664
  var stringifyString = require_stringifyString();
6658
6665
  var string = {
@@ -6668,7 +6675,7 @@ var require_string = __commonJS((exports) => {
6668
6675
  exports.string = string;
6669
6676
  });
6670
6677
 
6671
- // ../../../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
6672
6679
  var require_null = __commonJS((exports) => {
6673
6680
  var Scalar = require_Scalar();
6674
6681
  var nullTag = {
@@ -6683,7 +6690,7 @@ var require_null = __commonJS((exports) => {
6683
6690
  exports.nullTag = nullTag;
6684
6691
  });
6685
6692
 
6686
- // ../../../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
6687
6694
  var require_bool = __commonJS((exports) => {
6688
6695
  var Scalar = require_Scalar();
6689
6696
  var boolTag = {
@@ -6704,7 +6711,7 @@ var require_bool = __commonJS((exports) => {
6704
6711
  exports.boolTag = boolTag;
6705
6712
  });
6706
6713
 
6707
- // ../../../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
6708
6715
  var require_stringifyNumber = __commonJS((exports) => {
6709
6716
  function stringifyNumber({ format, minFractionDigits, tag, value }) {
6710
6717
  if (typeof value === "bigint")
@@ -6728,7 +6735,7 @@ var require_stringifyNumber = __commonJS((exports) => {
6728
6735
  exports.stringifyNumber = stringifyNumber;
6729
6736
  });
6730
6737
 
6731
- // ../../../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
6732
6739
  var require_float = __commonJS((exports) => {
6733
6740
  var Scalar = require_Scalar();
6734
6741
  var stringifyNumber = require_stringifyNumber();
@@ -6771,7 +6778,7 @@ var require_float = __commonJS((exports) => {
6771
6778
  exports.floatNaN = floatNaN;
6772
6779
  });
6773
6780
 
6774
- // ../../../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
6775
6782
  var require_int = __commonJS((exports) => {
6776
6783
  var stringifyNumber = require_stringifyNumber();
6777
6784
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
@@ -6813,7 +6820,7 @@ var require_int = __commonJS((exports) => {
6813
6820
  exports.intOct = intOct;
6814
6821
  });
6815
6822
 
6816
- // ../../../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
6817
6824
  var require_schema = __commonJS((exports) => {
6818
6825
  var map = require_map();
6819
6826
  var _null = require_null();
@@ -6838,7 +6845,7 @@ var require_schema = __commonJS((exports) => {
6838
6845
  exports.schema = schema;
6839
6846
  });
6840
6847
 
6841
- // ../../../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
6842
6849
  var require_schema2 = __commonJS((exports) => {
6843
6850
  var Scalar = require_Scalar();
6844
6851
  var map = require_map();
@@ -6902,7 +6909,7 @@ var require_schema2 = __commonJS((exports) => {
6902
6909
  exports.schema = schema;
6903
6910
  });
6904
6911
 
6905
- // ../../../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
6906
6913
  var require_binary = __commonJS((exports) => {
6907
6914
  var node_buffer = __require("buffer");
6908
6915
  var Scalar = require_Scalar();
@@ -6957,7 +6964,7 @@ var require_binary = __commonJS((exports) => {
6957
6964
  exports.binary = binary;
6958
6965
  });
6959
6966
 
6960
- // ../../../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
6961
6968
  var require_pairs = __commonJS((exports) => {
6962
6969
  var identity = require_identity();
6963
6970
  var Pair = require_Pair();
@@ -7032,7 +7039,7 @@ ${cn.comment}` : item.comment;
7032
7039
  exports.resolvePairs = resolvePairs;
7033
7040
  });
7034
7041
 
7035
- // ../../../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
7036
7043
  var require_omap = __commonJS((exports) => {
7037
7044
  var identity = require_identity();
7038
7045
  var toJS = require_toJS();
@@ -7104,7 +7111,7 @@ var require_omap = __commonJS((exports) => {
7104
7111
  exports.omap = omap;
7105
7112
  });
7106
7113
 
7107
- // ../../../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
7108
7115
  var require_bool2 = __commonJS((exports) => {
7109
7116
  var Scalar = require_Scalar();
7110
7117
  function boolStringify({ value, source }, ctx) {
@@ -7133,7 +7140,7 @@ var require_bool2 = __commonJS((exports) => {
7133
7140
  exports.trueTag = trueTag;
7134
7141
  });
7135
7142
 
7136
- // ../../../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
7137
7144
  var require_float2 = __commonJS((exports) => {
7138
7145
  var Scalar = require_Scalar();
7139
7146
  var stringifyNumber = require_stringifyNumber();
@@ -7179,7 +7186,7 @@ var require_float2 = __commonJS((exports) => {
7179
7186
  exports.floatNaN = floatNaN;
7180
7187
  });
7181
7188
 
7182
- // ../../../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
7183
7190
  var require_int2 = __commonJS((exports) => {
7184
7191
  var stringifyNumber = require_stringifyNumber();
7185
7192
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
@@ -7255,7 +7262,7 @@ var require_int2 = __commonJS((exports) => {
7255
7262
  exports.intOct = intOct;
7256
7263
  });
7257
7264
 
7258
- // ../../../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
7259
7266
  var require_set = __commonJS((exports) => {
7260
7267
  var identity = require_identity();
7261
7268
  var Pair = require_Pair();
@@ -7338,7 +7345,7 @@ var require_set = __commonJS((exports) => {
7338
7345
  exports.set = set;
7339
7346
  });
7340
7347
 
7341
- // ../../../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
7342
7349
  var require_timestamp = __commonJS((exports) => {
7343
7350
  var stringifyNumber = require_stringifyNumber();
7344
7351
  function parseSexagesimal(str, asBigInt) {
@@ -7420,7 +7427,7 @@ var require_timestamp = __commonJS((exports) => {
7420
7427
  exports.timestamp = timestamp;
7421
7428
  });
7422
7429
 
7423
- // ../../../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
7424
7431
  var require_schema3 = __commonJS((exports) => {
7425
7432
  var map = require_map();
7426
7433
  var _null = require_null();
@@ -7461,7 +7468,7 @@ var require_schema3 = __commonJS((exports) => {
7461
7468
  exports.schema = schema;
7462
7469
  });
7463
7470
 
7464
- // ../../../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
7465
7472
  var require_tags = __commonJS((exports) => {
7466
7473
  var map = require_map();
7467
7474
  var _null = require_null();
@@ -7552,7 +7559,7 @@ var require_tags = __commonJS((exports) => {
7552
7559
  exports.getTags = getTags;
7553
7560
  });
7554
7561
 
7555
- // ../../../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
7556
7563
  var require_Schema = __commonJS((exports) => {
7557
7564
  var identity = require_identity();
7558
7565
  var map = require_map();
@@ -7582,7 +7589,7 @@ var require_Schema = __commonJS((exports) => {
7582
7589
  exports.Schema = Schema;
7583
7590
  });
7584
7591
 
7585
- // ../../../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
7586
7593
  var require_stringifyDocument = __commonJS((exports) => {
7587
7594
  var identity = require_identity();
7588
7595
  var stringify = require_stringify();
@@ -7662,7 +7669,7 @@ var require_stringifyDocument = __commonJS((exports) => {
7662
7669
  exports.stringifyDocument = stringifyDocument;
7663
7670
  });
7664
7671
 
7665
- // ../../../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
7666
7673
  var require_Document = __commonJS((exports) => {
7667
7674
  var Alias = require_Alias();
7668
7675
  var Collection = require_Collection();
@@ -7897,7 +7904,7 @@ var require_Document = __commonJS((exports) => {
7897
7904
  exports.Document = Document;
7898
7905
  });
7899
7906
 
7900
- // ../../../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
7901
7908
  var require_errors = __commonJS((exports) => {
7902
7909
  class YAMLError extends Error {
7903
7910
  constructor(name, pos, code, message) {
@@ -7962,7 +7969,7 @@ ${pointer}
7962
7969
  exports.prettifyError = prettifyError;
7963
7970
  });
7964
7971
 
7965
- // ../../../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
7966
7973
  var require_resolve_props = __commonJS((exports) => {
7967
7974
  function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) {
7968
7975
  let spaceBefore = false;
@@ -8092,7 +8099,7 @@ var require_resolve_props = __commonJS((exports) => {
8092
8099
  exports.resolveProps = resolveProps;
8093
8100
  });
8094
8101
 
8095
- // ../../../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
8096
8103
  var require_util_contains_newline = __commonJS((exports) => {
8097
8104
  function containsNewline(key) {
8098
8105
  if (!key)
@@ -8132,7 +8139,7 @@ var require_util_contains_newline = __commonJS((exports) => {
8132
8139
  exports.containsNewline = containsNewline;
8133
8140
  });
8134
8141
 
8135
- // ../../../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
8136
8143
  var require_util_flow_indent_check = __commonJS((exports) => {
8137
8144
  var utilContainsNewline = require_util_contains_newline();
8138
8145
  function flowIndentCheck(indent, fc, onError) {
@@ -8147,7 +8154,7 @@ var require_util_flow_indent_check = __commonJS((exports) => {
8147
8154
  exports.flowIndentCheck = flowIndentCheck;
8148
8155
  });
8149
8156
 
8150
- // ../../../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
8151
8158
  var require_util_map_includes = __commonJS((exports) => {
8152
8159
  var identity = require_identity();
8153
8160
  function mapIncludes(ctx, items, search) {
@@ -8160,7 +8167,7 @@ var require_util_map_includes = __commonJS((exports) => {
8160
8167
  exports.mapIncludes = mapIncludes;
8161
8168
  });
8162
8169
 
8163
- // ../../../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
8164
8171
  var require_resolve_block_map = __commonJS((exports) => {
8165
8172
  var Pair = require_Pair();
8166
8173
  var YAMLMap = require_YAMLMap();
@@ -8267,7 +8274,7 @@ var require_resolve_block_map = __commonJS((exports) => {
8267
8274
  exports.resolveBlockMap = resolveBlockMap;
8268
8275
  });
8269
8276
 
8270
- // ../../../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
8271
8278
  var require_resolve_block_seq = __commonJS((exports) => {
8272
8279
  var YAMLSeq = require_YAMLSeq();
8273
8280
  var resolveProps = require_resolve_props();
@@ -8315,7 +8322,7 @@ var require_resolve_block_seq = __commonJS((exports) => {
8315
8322
  exports.resolveBlockSeq = resolveBlockSeq;
8316
8323
  });
8317
8324
 
8318
- // ../../../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
8319
8326
  var require_resolve_end = __commonJS((exports) => {
8320
8327
  function resolveEnd(end, offset, reqSpace, onError) {
8321
8328
  let comment = "";
@@ -8355,7 +8362,7 @@ var require_resolve_end = __commonJS((exports) => {
8355
8362
  exports.resolveEnd = resolveEnd;
8356
8363
  });
8357
8364
 
8358
- // ../../../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
8359
8366
  var require_resolve_flow_collection = __commonJS((exports) => {
8360
8367
  var identity = require_identity();
8361
8368
  var Pair = require_Pair();
@@ -8546,7 +8553,7 @@ var require_resolve_flow_collection = __commonJS((exports) => {
8546
8553
  exports.resolveFlowCollection = resolveFlowCollection;
8547
8554
  });
8548
8555
 
8549
- // ../../../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
8550
8557
  var require_compose_collection = __commonJS((exports) => {
8551
8558
  var identity = require_identity();
8552
8559
  var Scalar = require_Scalar();
@@ -8608,7 +8615,7 @@ var require_compose_collection = __commonJS((exports) => {
8608
8615
  exports.composeCollection = composeCollection;
8609
8616
  });
8610
8617
 
8611
- // ../../../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
8612
8619
  var require_resolve_block_scalar = __commonJS((exports) => {
8613
8620
  var Scalar = require_Scalar();
8614
8621
  function resolveBlockScalar(ctx, scalar, onError) {
@@ -8801,7 +8808,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
8801
8808
  exports.resolveBlockScalar = resolveBlockScalar;
8802
8809
  });
8803
8810
 
8804
- // ../../../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
8805
8812
  var require_resolve_flow_scalar = __commonJS((exports) => {
8806
8813
  var Scalar = require_Scalar();
8807
8814
  var resolveEnd = require_resolve_end();
@@ -9018,7 +9025,7 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
9018
9025
  exports.resolveFlowScalar = resolveFlowScalar;
9019
9026
  });
9020
9027
 
9021
- // ../../../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
9022
9029
  var require_compose_scalar = __commonJS((exports) => {
9023
9030
  var identity = require_identity();
9024
9031
  var Scalar = require_Scalar();
@@ -9096,7 +9103,7 @@ var require_compose_scalar = __commonJS((exports) => {
9096
9103
  exports.composeScalar = composeScalar;
9097
9104
  });
9098
9105
 
9099
- // ../../../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
9100
9107
  var require_util_empty_scalar_position = __commonJS((exports) => {
9101
9108
  function emptyScalarPosition(offset, before, pos) {
9102
9109
  if (before) {
@@ -9123,7 +9130,7 @@ var require_util_empty_scalar_position = __commonJS((exports) => {
9123
9130
  exports.emptyScalarPosition = emptyScalarPosition;
9124
9131
  });
9125
9132
 
9126
- // ../../../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
9127
9134
  var require_compose_node = __commonJS((exports) => {
9128
9135
  var Alias = require_Alias();
9129
9136
  var identity = require_identity();
@@ -9226,7 +9233,7 @@ var require_compose_node = __commonJS((exports) => {
9226
9233
  exports.composeNode = composeNode;
9227
9234
  });
9228
9235
 
9229
- // ../../../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
9230
9237
  var require_compose_doc = __commonJS((exports) => {
9231
9238
  var Document = require_Document();
9232
9239
  var composeNode = require_compose_node();
@@ -9266,7 +9273,7 @@ var require_compose_doc = __commonJS((exports) => {
9266
9273
  exports.composeDoc = composeDoc;
9267
9274
  });
9268
9275
 
9269
- // ../../../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
9270
9277
  var require_composer = __commonJS((exports) => {
9271
9278
  var node_process = __require("process");
9272
9279
  var directives = require_directives();
@@ -9457,7 +9464,7 @@ ${end.comment}` : end.comment;
9457
9464
  exports.Composer = Composer;
9458
9465
  });
9459
9466
 
9460
- // ../../../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
9461
9468
  var require_cst_scalar = __commonJS((exports) => {
9462
9469
  var resolveBlockScalar = require_resolve_block_scalar();
9463
9470
  var resolveFlowScalar = require_resolve_flow_scalar();
@@ -9647,7 +9654,7 @@ var require_cst_scalar = __commonJS((exports) => {
9647
9654
  exports.setScalarValue = setScalarValue;
9648
9655
  });
9649
9656
 
9650
- // ../../../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
9651
9658
  var require_cst_stringify = __commonJS((exports) => {
9652
9659
  var stringify = (cst) => ("type" in cst) ? stringifyToken(cst) : stringifyItem(cst);
9653
9660
  function stringifyToken(token) {
@@ -9705,7 +9712,7 @@ var require_cst_stringify = __commonJS((exports) => {
9705
9712
  exports.stringify = stringify;
9706
9713
  });
9707
9714
 
9708
- // ../../../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
9709
9716
  var require_cst_visit = __commonJS((exports) => {
9710
9717
  var BREAK = Symbol("break visit");
9711
9718
  var SKIP = Symbol("skip children");
@@ -9764,7 +9771,7 @@ var require_cst_visit = __commonJS((exports) => {
9764
9771
  exports.visit = visit;
9765
9772
  });
9766
9773
 
9767
- // ../../../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
9768
9775
  var require_cst = __commonJS((exports) => {
9769
9776
  var cstScalar = require_cst_scalar();
9770
9777
  var cstStringify = require_cst_stringify();
@@ -9865,7 +9872,7 @@ var require_cst = __commonJS((exports) => {
9865
9872
  exports.tokenType = tokenType;
9866
9873
  });
9867
9874
 
9868
- // ../../../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
9869
9876
  var require_lexer = __commonJS((exports) => {
9870
9877
  var cst = require_cst();
9871
9878
  function isEmpty(ch) {
@@ -10462,7 +10469,7 @@ var require_lexer = __commonJS((exports) => {
10462
10469
  exports.Lexer = Lexer;
10463
10470
  });
10464
10471
 
10465
- // ../../../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
10466
10473
  var require_line_counter = __commonJS((exports) => {
10467
10474
  class LineCounter {
10468
10475
  constructor() {
@@ -10490,7 +10497,7 @@ var require_line_counter = __commonJS((exports) => {
10490
10497
  exports.LineCounter = LineCounter;
10491
10498
  });
10492
10499
 
10493
- // ../../../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
10494
10501
  var require_parser = __commonJS((exports) => {
10495
10502
  var node_process = __require("process");
10496
10503
  var cst = require_cst();
@@ -11346,7 +11353,7 @@ var require_parser = __commonJS((exports) => {
11346
11353
  exports.Parser = Parser;
11347
11354
  });
11348
11355
 
11349
- // ../../../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
11350
11357
  var require_public_api = __commonJS((exports) => {
11351
11358
  var composer = require_composer();
11352
11359
  var Document = require_Document();
@@ -11440,7 +11447,7 @@ var require_public_api = __commonJS((exports) => {
11440
11447
  exports.stringify = stringify;
11441
11448
  });
11442
11449
 
11443
- // ../../../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
11444
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;
11445
11452
  var init_dist = __esm(() => {
11446
11453
  composer = require_composer();
@@ -11489,7 +11496,7 @@ var init_dist = __esm(() => {
11489
11496
  $visitAsync = visit.visitAsync;
11490
11497
  });
11491
11498
 
11492
- // ../manifest/src/loader.ts
11499
+ // packages/libs/manifest/src/loader.ts
11493
11500
  import { createHash } from "crypto";
11494
11501
  import { readFile as readFile2, realpath as realpath2 } from "fs/promises";
11495
11502
  import path2 from "path";
@@ -11676,12 +11683,13 @@ var init_loader = __esm(() => {
11676
11683
  };
11677
11684
  });
11678
11685
 
11679
- // ../manifest/src/index.ts
11686
+ // packages/libs/manifest/src/index.ts
11680
11687
  var exports_src = {};
11681
11688
  __export(exports_src, {
11682
11689
  sandblocksManifestDigest: () => sandblocksManifestDigest,
11683
11690
  resolveServiceSteps: () => resolveServiceSteps,
11684
11691
  resolveProjectGraph: () => resolveProjectGraph,
11692
+ resolvePrebuiltImage: () => resolvePrebuiltImage,
11685
11693
  resolveEnvironment: () => resolveEnvironment,
11686
11694
  parseSandblocksManifest: () => parseSandblocksManifest,
11687
11695
  normalizeCommand: () => normalizeCommand,
@@ -11709,1708 +11717,2153 @@ var init_src = __esm(() => {
11709
11717
  init_loader();
11710
11718
  });
11711
11719
 
11712
- // src/cli.ts
11720
+ // packages/libs/cli/src/cli.ts
11713
11721
  init_src();
11714
11722
  import { randomUUID as randomUUID4 } from "crypto";
11715
- 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";
11716
11724
  import path6 from "path";
11717
11725
 
11718
- // src/hooks.ts
11719
- import { chmod, copyFile, mkdir, readFile as readFile3, rm, writeFile } from "fs/promises";
11720
- import path3 from "path";
11721
- var marker = "# sandblocks-managed-post-commit";
11722
- var backupName = "post-commit.before-sandblocks";
11723
- async function runHooksCommand(args, dependencies) {
11724
- const [command = "status", ...rest] = args;
11725
- if (command === "run") {
11726
- const [hook, directory] = rest;
11727
- if (hook !== "post-commit")
11728
- throw new Error("hooks run supports only post-commit");
11729
- 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";
11730
11740
  }
11731
- const root = path3.resolve(rest.find((value) => !value.startsWith("--")) ?? process.cwd());
11732
- if (command === "install")
11733
- return install(root);
11734
- if (command === "uninstall")
11735
- return uninstall(root);
11736
- if (command === "status")
11737
- return status(root);
11738
- throw new Error(`unsupported hooks command '${command}'`);
11739
- }
11740
- async function install(root) {
11741
- const hooksDir = await gitPath(root, "hooks");
11742
- const hookPath = path3.join(hooksDir, "post-commit");
11743
- const backupPath = path3.join(hooksDir, backupName);
11744
- await mkdir(hooksDir, { recursive: true });
11745
- const current = await text(hookPath);
11746
- if (current && !current.includes(marker)) {
11747
- await copyFile(hookPath, backupPath);
11748
- 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
+ }
11749
11804
  }
11750
- await writeFile(hookPath, `#!/bin/sh
11751
- ${marker}
11752
- set -u
11753
- previous="$(dirname "$0")/${backupName}"
11754
- if [ -x "$previous" ]; then "$previous" || exit $?; fi
11755
- root=$(git rev-parse --show-toplevel) || exit $?
11756
- cd "$root" || exit $?
11757
- if [ -x node_modules/.bin/sandblocks ]; then exec node_modules/.bin/sandblocks hooks run post-commit "$root"; fi
11758
- exec sandblocks hooks run post-commit "$root"
11759
- `);
11760
- await chmod(hookPath, 493);
11761
- console.log(`installed ${hookPath}`);
11762
- if (await exists(backupPath))
11763
- console.log(`chaining ${backupPath}`);
11764
- }
11765
- async function uninstall(root) {
11766
- const hooksDir = await gitPath(root, "hooks");
11767
- const hookPath = path3.join(hooksDir, "post-commit");
11768
- const backupPath = path3.join(hooksDir, backupName);
11769
- const current = await text(hookPath);
11770
- if (!current?.includes(marker))
11771
- throw new Error("Sandblocks post-commit hook is not installed");
11772
- if (await exists(backupPath)) {
11773
- await copyFile(backupPath, hookPath);
11774
- await chmod(hookPath, 493);
11775
- await rm(backupPath);
11776
- console.log(`restored ${hookPath}`);
11777
- } else {
11778
- await rm(hookPath);
11779
- 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
+ };
11780
11817
  }
11781
- }
11782
- async function status(root) {
11783
- const hooksDir = await gitPath(root, "hooks");
11784
- const installed = (await text(path3.join(hooksDir, "post-commit")))?.includes(marker) === true;
11785
- const loaded = await loadPolicy(root);
11786
- console.log(`post-commit ${installed ? "installed" : "not installed"}`);
11787
- console.log(`policy ${loaded ? loaded.enabled ? "enabled" : "disabled" : "not configured"}`);
11788
- if (loaded) {
11789
- console.log(`branches ${loaded.branches.join(",")}`);
11790
- console.log(`environment ${loaded.environment}`);
11791
- 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);
11792
11831
  }
11793
11832
  }
11794
- async function runPostCommit(root, dependencies) {
11795
- if (process.env.SANDBLOCKS_SKIP_POST_COMMIT === "1") {
11796
- console.log("sandblocks post-commit skipped by SANDBLOCKS_SKIP_POST_COMMIT");
11797
- return;
11798
- }
11799
- const policy = (await dependencies.load(root)).manifest.hooks.postCommit;
11800
- if (!policy?.enabled)
11801
- return;
11802
- const branch = await git(root, ["branch", "--show-current"]);
11803
- if (!policy.branches.includes(branch)) {
11804
- console.log(`sandblocks post-commit skipped on branch ${branch || "(detached)"}`);
11805
- return;
11806
- }
11807
- const stateFile = path3.join(root, ".sandblocks", `sandbox-${policy.environment}.json`);
11808
- const command = await exists(stateFile) ? "redeploy" : "up";
11809
- const commandArgs = [root, "--environment", policy.environment];
11810
- if (policy.background) {
11811
- const logDir = path3.join(root, ".sandblocks", "logs");
11812
- await mkdir(logDir, { recursive: true });
11813
- const log = path3.join(logDir, `post-commit-${Date.now()}.log`);
11814
- const executable = process.argv[1];
11815
- if (!executable)
11816
- throw new Error("unable to resolve Sandblocks CLI executable");
11817
- const proc = Bun.spawn([process.execPath, executable, "sandbox", command, ...commandArgs], {
11818
- cwd: root,
11819
- stdout: Bun.file(log),
11820
- stderr: Bun.file(log),
11821
- env: { ...process.env, SANDBLOCKS_SKIP_POST_COMMIT: "1" }
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
11822
11850
  });
11823
- proc.unref();
11824
- console.log(`sandblocks ${command} started for ${policy.environment} (${log})`);
11825
- return;
11826
11851
  }
11827
- await dependencies.runSandbox(command, commandArgs);
11828
- }
11829
- async function loadPolicy(root) {
11830
- const { loadSandblocksManifest: loadSandblocksManifest2 } = await Promise.resolve().then(() => (init_src(), exports_src));
11831
- return (await loadSandblocksManifest2({ repositoryRoot: root })).manifest.hooks.postCommit;
11832
- }
11833
- async function gitPath(root, name) {
11834
- return path3.resolve(root, await git(root, ["rev-parse", "--git-path", name]));
11835
- }
11836
- async function git(root, args) {
11837
- const proc = Bun.spawn(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
11838
- const [stdout, stderr, code] = await Promise.all([
11839
- new Response(proc.stdout).text(),
11840
- new Response(proc.stderr).text(),
11841
- proc.exited
11842
- ]);
11843
- if (code !== 0)
11844
- throw new Error(stderr.trim() || `git ${args.join(" ")} failed`);
11845
- return stdout.trim();
11846
- }
11847
- async function exists(file) {
11848
- return Bun.file(file).exists();
11849
11852
  }
11850
- async function text(file) {
11851
- try {
11852
- return await readFile3(file, "utf8");
11853
- } catch {
11854
- return;
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
11870
+ });
11855
11871
  }
11856
11872
  }
11857
11873
 
11858
- // src/runtime.ts
11859
- import { spawn } from "child_process";
11860
- import { access, chmod as chmod2, mkdir as mkdir2, readdir, rm as rm2 } from "fs/promises";
11861
- import path4 from "path";
11862
- var RUNTIME_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
11863
- var NODE_ENV_RUNTIME = {
11864
- production: "prod",
11865
- development: "dev",
11866
- test: "local"
11867
- };
11868
- async function runRuntimeCommand(args) {
11869
- const [action, ...rest] = args;
11870
- if (action !== "prepare")
11871
- throw new Error("runtime action must be prepare");
11872
- const options = parse(rest);
11873
- const root = path4.resolve(options.positionals[0] ?? process.cwd());
11874
- const runtime = normalizedRuntime(option(options, "runtime") ?? process.env.SANDBLOCKS_RUNTIME ?? "development" ?? "development");
11875
- const targetRoot = contained(root, option(options, "target") ?? ".locker", "runtime target");
11876
- const runtimeRoot = path4.join(targetRoot, runtime);
11877
- const config = contained(root, option(options, "config") ?? "config/dotlocker.config.ts", "Dotlocker config");
11878
- const dotlocker = path4.resolve(root, option(options, "dotlocker") ?? path4.join("node_modules", ".bin", "dotlocker"));
11879
- if (!await exists2(config))
11880
- throw new Error(`Dotlocker config does not exist: ${config}`);
11881
- if (!await exists2(dotlocker)) {
11882
- throw new Error("Dotlocker is not installed; add @dotlocker/dotlocker to the workspace");
11883
- }
11884
- await rm2(targetRoot, { recursive: true, force: true });
11885
- await mkdir2(runtimeRoot, { recursive: true, mode: 493 });
11886
- await run(dotlocker, ["pull", "--config", config, "--runtime", runtime, "--out", runtimeRoot], root);
11887
- const envFile = path4.join(runtimeRoot, ".env");
11888
- if (!await exists2(envFile)) {
11889
- throw new Error(`Dotlocker did not materialize ${path4.relative(root, envFile)}`);
11890
- }
11891
- await normalizePermissions(targetRoot);
11892
- console.log(`runtime ${runtime}
11893
- environment ${path4.relative(root, envFile)}`);
11894
- }
11895
- function run(executable, args, cwd) {
11896
- return new Promise((resolve, reject) => {
11897
- const child = spawn(executable, args, { cwd, env: process.env, stdio: "inherit" });
11898
- child.once("error", reject);
11899
- child.once("exit", (code, signal) => {
11900
- if (code === 0)
11901
- resolve();
11902
- else
11903
- reject(new Error(`Dotlocker pull failed (${signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`})`));
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
11904
11890
  });
11905
- });
11891
+ }
11906
11892
  }
11907
- async function exists2(file) {
11893
+ function parseJsonAgentEvent(line, context) {
11894
+ let value;
11908
11895
  try {
11909
- await access(file);
11910
- return true;
11896
+ value = JSON.parse(line);
11911
11897
  } catch {
11912
- return false;
11898
+ return { type: "raw", line, ...context };
11899
+ }
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
+ };
11913
11908
  }
11909
+ const text = value.text ?? value.message ?? value.content;
11910
+ return typeof text === "string" ? { type: "text", text, ...context } : { type: "raw", line, ...context };
11914
11911
  }
11915
- function normalizedRuntime(value) {
11916
- const runtime = NODE_ENV_RUNTIME[value.trim()] ?? value.trim();
11917
- if (!RUNTIME_PATTERN.test(runtime))
11918
- throw new Error(`invalid runtime '${value}'`);
11919
- 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 };
11920
11924
  }
11921
- function contained(root, value, label) {
11922
- const resolved = path4.resolve(root, value);
11923
- const relative = path4.relative(root, resolved);
11924
- if (!relative || !relative.startsWith("..") && !path4.isAbsolute(relative))
11925
- return resolved;
11926
- 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];
11927
11929
  }
11928
- async function normalizePermissions(directory) {
11929
- await chmod2(directory, 493);
11930
- for (const entry of await readdir(directory, { withFileTypes: true })) {
11931
- const child = path4.join(directory, entry.name);
11932
- if (entry.isSymbolicLink())
11933
- throw new Error("Dotlocker output must not contain symbolic links");
11934
- if (entry.isDirectory())
11935
- await normalizePermissions(child);
11936
- else if (entry.isFile())
11937
- await chmod2(child, 420);
11938
- else
11939
- 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 };
11940
11936
  }
11941
- }
11942
- function parse(args) {
11943
- const options = { positionals: [] };
11944
- for (let index = 0;index < args.length; index += 1) {
11945
- const value = args[index] ?? "";
11946
- if (!value.startsWith("--")) {
11947
- options.positionals.push(value);
11948
- continue;
11949
- }
11950
- const key = value.slice(2);
11951
- const next = args[index + 1];
11952
- if (next && !next.startsWith("--")) {
11953
- options[key] = next;
11954
- index += 1;
11955
- } else
11956
- 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`);
11957
11953
  }
11958
- return options;
11959
- }
11960
- function option(options, key) {
11961
- const value = options[key];
11962
- return typeof value === "string" ? value : undefined;
11963
- }
11964
-
11965
- // src/sandbox.ts
11966
- init_src();
11967
- import { createHash as createHash2, randomUUID } from "crypto";
11968
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rename, rm as rm3 } from "fs/promises";
11969
- import path5 from "path";
11970
- var COMMANDS = new Set([
11971
- "up",
11972
- "create",
11973
- "status",
11974
- "deploy",
11975
- "redeploy",
11976
- "renew",
11977
- "exec",
11978
- "git",
11979
- "down",
11980
- "destroy"
11981
- ]);
11982
- function isStatefulSandboxCommand(command) {
11983
- return Boolean(command && COMMANDS.has(command));
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");
11984
11970
  }
11985
- async function runStatefulSandboxCommand(command, args, dependencies) {
11986
- if (command === "up")
11987
- return up(args, dependencies);
11988
- if (command === "create")
11989
- return create(args, dependencies);
11990
- if (command === "status")
11991
- return status2(args, dependencies);
11992
- if (command === "deploy")
11993
- return deploy(args, dependencies);
11994
- if (command === "redeploy")
11995
- return redeploy(args, dependencies);
11996
- if (command === "renew")
11997
- return renew(args, dependencies);
11998
- if (command === "exec")
11999
- return exec(args, dependencies);
12000
- if (command === "git")
12001
- return git2(args, dependencies);
12002
- return down(args, dependencies);
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;
12003
11975
  }
12004
- async function up(args, dependencies) {
12005
- await withDeploymentLock(args, dependencies, async () => {
12006
- await create(args, dependencies);
12007
- try {
12008
- await deployUnlocked(args, dependencies);
12009
- } catch (error) {
12010
- if (process.env.SANDBLOCKS_RETAIN_FAILED !== "true") {
12011
- await down(args, dependencies).catch(() => {
12012
- return;
12013
- });
12014
- }
12015
- throw error;
12016
- }
12017
- });
11976
+ function escapeRegex(value) {
11977
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12018
11978
  }
12019
- async function create(args, dependencies) {
12020
- const options = dependencies.parse(args);
12021
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12022
- await loadLocalEnvironment(root);
12023
- const environment = dependencies.option(options, "environment") ?? "preview";
12024
- const file = statePath(root, environment);
12025
- if (await Bun.file(file).exists())
12026
- throw new Error(`sandbox '${environment}' already exists`);
12027
- const loaded = await dependencies.load(root, dependencies.option(options, "config") ?? dependencies.option(options, "manifest"));
12028
- const preview = selectPreview(loaded, environment);
12029
- const projectId = dependencies.option(options, "project") ?? process.env.SANDBLOCKS_PROJECT_ID ?? "default";
12030
- const repository = loaded.manifest.name ?? path5.basename(root).toLowerCase();
12031
- const sandboxId = randomUUID();
12032
- const workspaceId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${repository}`.slice(0, 63);
12033
- const { apiUrl, apiKey } = client(options, dependencies.option, projectId);
12034
- await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes`, {
12035
- method: "POST",
12036
- body: JSON.stringify({ id: sandboxId, goal: preview.goal, status: "active", repositories: [repository] })
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"]
12037
11991
  });
12038
- try {
12039
- const lease = await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}/leases`, {
12040
- method: "POST",
12041
- body: JSON.stringify({
12042
- subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
12043
- scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
12044
- ttlSeconds: Number(dependencies.option(options, "ttl-seconds") ?? 28800)
12045
- })
12046
- });
12047
- const imported = await importSource({
12048
- root,
12049
- projectId,
12050
- sandboxId,
12051
- workspaceId,
12052
- pool: dependencies.option(options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
12053
- apiUrl,
12054
- apiKey,
12055
- idempotencyKey: `${sandboxId}:workspace-import`,
12056
- source: dependencies.source,
12057
- ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
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;
12058
12003
  });
12059
- const hostId = String(imported.operation.assignedHostId ?? "");
12060
- if (!hostId)
12061
- throw new Error("workspace import did not bind a sandbox host");
12062
- const state = {
12063
- version: 1,
12064
- environment,
12065
- projectId,
12066
- sandboxId,
12067
- workspaceId,
12068
- repository,
12069
- leaseId: lease.lease.id,
12070
- leaseToken: lease.token,
12071
- hostId,
12072
- sequence: 0,
12073
- createdAt: new Date().toISOString()
12074
- };
12075
- await writeState(file, state);
12076
- console.log(`sandbox ${sandboxId}`);
12077
- console.log(`workspace ${workspaceId}`);
12078
- console.log(`host ${hostId}`);
12079
- } catch (error) {
12080
- await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
12081
- method: "DELETE"
12082
- }).catch(() => {
12004
+ });
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(() => {
12083
12009
  return;
12084
12010
  });
12085
- throw error;
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)));
12022
+ });
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()
12033
+ };
12034
+ } finally {
12035
+ if (timer)
12036
+ clearTimeout(timer);
12037
+ options.signal?.removeEventListener("abort", stop);
12086
12038
  }
12087
12039
  }
12088
- async function status2(args, dependencies) {
12089
- const context = await getContext(args, dependencies);
12090
- const [sandbox, session] = await Promise.all([
12091
- request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`),
12092
- leaseRequest(context, "/v1/sandbox-session")
12093
- ]);
12094
- if (context.options.json) {
12095
- console.log(JSON.stringify({ state: redactedState(context.state), sandbox: sandbox.sandbox, session: session.session }, null, 2));
12096
- 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;
12097
12058
  }
12098
- console.log(`state ${path5.relative(context.root, context.file)}`);
12099
- console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
12100
- console.log(`workspace ${context.state.workspaceId}`);
12101
- console.log(`host ${context.state.hostId}`);
12102
- console.log(`lease expires ${session.session.expiresAt}`);
12103
- for (const route of context.state.previewUrls ?? [])
12104
- console.log(`${route.service ?? "service"} ${route.url}`);
12105
- }
12106
- async function deploy(args, dependencies) {
12107
- await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
12108
- }
12109
- async function withDeploymentLock(args, dependencies, action) {
12110
- const options = dependencies.parse(args);
12111
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12112
- const environment = dependencies.option(options, "environment") ?? "preview";
12113
- const lock = `${statePath(root, environment)}.deploy.lock`;
12114
- await mkdir3(path5.dirname(lock), { recursive: true, mode: 448 });
12115
- try {
12116
- await mkdir3(lock);
12117
- } catch (error) {
12118
- if (error.code === "EEXIST") {
12119
- throw new Error(`sandbox '${environment}' deployment is already running`);
12120
- }
12121
- throw error;
12059
+ exec(command, options = {}) {
12060
+ return this.runtime.exec(command, options);
12122
12061
  }
12123
- try {
12124
- await action();
12125
- } finally {
12126
- 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);
12127
12066
  }
12128
- }
12129
- async function deployUnlocked(args, dependencies) {
12130
- const context = await getContext(args, dependencies);
12131
- if (context.state.deploymentId)
12132
- throw new Error("preview is already deployed; use sandbox redeploy");
12133
- const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
12134
- const stack = loaded.manifest.name ?? context.state.repository;
12135
- const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
12136
- const targetHost = await resolveAssignedTargetHost(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
12137
- if (!targetHost)
12138
- throw new Error("assigned host targetHost label or preview targetHost is required");
12139
- const deploymentId = randomUUID();
12140
- const environmentSnapshotId = await createEnvironmentSnapshot(context);
12141
- const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
12142
- const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
12143
- if (serviceSteps.length) {
12144
- const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12145
- method: "POST",
12146
- headers: {
12147
- "idempotency-key": `preview:${deploymentId}:steps`
12148
- },
12149
- body: JSON.stringify({
12150
- kind: "workspace.step.run",
12151
- stack: context.state.repository,
12152
- environmentId: context.state.environment,
12153
- environmentSnapshotId,
12154
- payload: {
12155
- workspaceId: context.state.workspaceId,
12156
- sandboxId: context.state.sandboxId,
12157
- revisionId: deploymentId,
12158
- runtimeType: preview.deploymentType,
12159
- serviceUrls,
12160
- steps: serviceSteps,
12161
- placement: { hostId: context.state.hostId }
12162
- },
12163
- maxAttempts: 1
12164
- })
12165
- });
12166
- 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);
12167
12071
  }
12168
- const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12169
- method: "POST",
12170
- headers: {
12171
- "idempotency-key": `preview:${deploymentId}:deploy`
12172
- },
12173
- body: JSON.stringify({
12174
- kind: "workspace.service.deploy",
12175
- stack: context.state.repository,
12176
- environmentId: context.state.environment,
12177
- environmentSnapshotId,
12178
- payload: {
12179
- workspaceId: context.state.workspaceId,
12180
- deploymentId,
12181
- sandboxId: context.state.sandboxId,
12182
- runtimeType: preview.deploymentType,
12183
- stack,
12184
- workspace: preview.workspace,
12185
- targetHost,
12186
- serviceUrls,
12187
- services: preview.services,
12188
- placement: { hostId: context.state.hostId }
12189
- },
12190
- maxAttempts: 1
12191
- })
12192
- });
12193
- const operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
12194
- context.state.deploymentId = deploymentId;
12195
- await writeState(context.file, context.state);
12196
- const routes = operation.result?.serviceRoutes ?? [];
12197
- const images = operation.result?.images ?? [];
12198
- if (typeof preview.ssoEnabled === "boolean") {
12199
- 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);
12200
12074
  }
12201
- let sandbox = await recordSandbox(context, loaded, preview, routes, operation.result?.healthEvidence ?? [], images, preview.steps.length ? "active" : "available");
12202
- let checkEvidence = [];
12203
- try {
12204
- if (preview.steps.length) {
12205
- const checkRoutes = sandbox.previewUrls ?? [];
12206
- await waitForPublishedRoutes(checkRoutes);
12207
- const serviceUrls2 = Object.fromEntries(checkRoutes.flatMap((route) => route.service ? [
12208
- [
12209
- `SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`,
12210
- route.url
12211
- ]
12212
- ] : []));
12213
- const check = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12214
- method: "POST",
12215
- headers: {
12216
- "idempotency-key": `preview:${deploymentId}:checks`
12217
- },
12218
- body: JSON.stringify({
12219
- kind: "workspace.check.run",
12220
- stack: context.state.repository,
12221
- environmentId: context.state.environment,
12222
- environmentSnapshotId,
12223
- payload: {
12224
- workspaceId: context.state.workspaceId,
12225
- deploymentId,
12226
- sandboxId: context.state.sandboxId,
12227
- runtimeType: preview.deploymentType,
12228
- checks: preview.steps,
12229
- serviceUrls: serviceUrls2,
12230
- placement: { hostId: context.state.hostId }
12231
- },
12232
- maxAttempts: 1
12233
- })
12234
- });
12235
- const checked = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
12236
- checkEvidence = checked.result?.checks ?? [];
12237
- if (checked.result?.passed !== true) {
12238
- 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;
12239
12083
  }
12240
- 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;
12241
12095
  }
12242
- } catch (error) {
12243
- await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
12244
- return;
12245
- });
12246
- await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
12247
- return;
12248
- });
12249
- throw error;
12250
12096
  }
12251
- await releaseEnvironmentSnapshot(context, environmentSnapshotId);
12252
- context.state.deploymentId = deploymentId;
12253
- const previewUrls = sandbox.previewUrls ?? [];
12254
- context.state.previewUrls = previewUrls;
12255
- await writeState(context.file, context.state);
12256
- console.log(`deployment ${deploymentId} available`);
12257
- for (const route of previewUrls)
12258
- console.log(`${route.service ?? "service"} ${route.url}`);
12259
- }
12260
- async function redeploy(args, dependencies) {
12261
- const context = await getContext(args, dependencies);
12262
- await renewLease(context, dependencies);
12263
- const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
12264
- const previousDeploymentId = context.state.deploymentId;
12265
- const previousUrls = context.state.previewUrls;
12266
- const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...candidateState } = context.state;
12267
- context.state = candidateState;
12268
- await writeState(context.file, context.state);
12269
- try {
12270
- await destroyWorkspace(context, `${context.state.sandboxId}:workspace-redeploy-destroy:${Date.now()}`);
12271
- const imported = await importSource({
12272
- root: context.root,
12273
- projectId: context.state.projectId,
12274
- sandboxId: context.state.sandboxId,
12275
- workspaceId: context.state.workspaceId,
12276
- pool: dependencies.option(context.options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
12277
- apiUrl: context.apiUrl,
12278
- apiKey: context.apiKey,
12279
- idempotencyKey: `${context.state.sandboxId}:workspace-redeploy:${Date.now()}`,
12280
- source: dependencies.source,
12281
- ..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
12282
- });
12283
- const hostId = String(imported.operation.assignedHostId ?? "");
12284
- if (!hostId)
12285
- throw new Error("workspace re-import did not bind a sandbox host");
12286
- context.state.hostId = hostId;
12287
- await writeState(context.file, context.state);
12288
- await deploy(args, dependencies);
12289
- if (previousDeploymentId)
12290
- await destroyDeploymentById(context, previousDeploymentId);
12291
- } catch (error) {
12292
- const failedCandidateId = context.state.deploymentId;
12293
- if (failedCandidateId && failedCandidateId !== previousDeploymentId) {
12294
- await destroyDeploymentById(context, failedCandidateId).catch(() => {
12295
- return;
12296
- });
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;
12297
12150
  }
12298
- context.state.deploymentId = previousDeploymentId;
12299
- context.state.previewUrls = previousUrls;
12300
- await writeState(context.file, context.state);
12301
- 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);
12302
12184
  }
12303
12185
  }
12304
- async function renew(args, dependencies) {
12305
- const context = await getContext(args, dependencies);
12306
- const lease = await renewLease(context, dependencies);
12307
- console.log(`lease ${lease.lease.id} renewed until ${lease.lease.expiresAt}`);
12308
- }
12309
- async function renewLease(context, dependencies) {
12310
- const previousLeaseId = context.state.leaseId;
12311
- const lease = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}/leases`, {
12312
- method: "POST",
12313
- body: JSON.stringify({
12314
- subject: `sandblocks-cli:${process.env.USER ?? "developer"}`,
12315
- scopes: ["sandbox:read", "workspace:read", "workspace:write", "workspace:exec", "workspace:git"],
12316
- ttlSeconds: Number(dependencies.option(context.options, "ttl-seconds") ?? 28800)
12317
- })
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
12318
12222
  });
12319
- context.state.leaseId = lease.lease.id;
12320
- context.state.leaseToken = lease.token;
12321
- await writeState(context.file, context.state);
12322
- if (previousLeaseId && previousLeaseId !== lease.lease.id) {
12323
- 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(() => {
12324
12232
  return;
12325
12233
  });
12234
+ throw error;
12326
12235
  }
12327
- return lease;
12328
12236
  }
12329
- async function exec(args, dependencies) {
12330
- const separator = args.indexOf("--");
12331
- if (separator < 0 || separator === args.length - 1)
12332
- throw new Error("sandbox exec requires -- followed by a command");
12333
- const context = await getContext(args.slice(0, separator), dependencies);
12334
- const command = args.slice(separator + 1);
12335
- const operation = await dataOperation(context, "exec", { executable: command[0], args: command.slice(1) });
12336
- 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 });
12337
12243
  }
12338
- async function git2(args, dependencies) {
12339
- const actionIndex = args.findIndex((value) => ["status", "diff", "stage", "checkout"].includes(value));
12340
- if (actionIndex < 0)
12341
- throw new Error("sandbox git action must be status, diff, stage, or checkout");
12342
- const context = await getContext(args.slice(0, actionIndex), dependencies);
12343
- const action = args[actionIndex];
12344
- if (!action)
12345
- throw new Error("sandbox git action is required");
12346
- const rest = args.slice(actionIndex + 1);
12347
- const operation = await dataOperation(context, "git", action === "checkout" ? { action, branch: rest[0] } : { action, ...rest.length ? { paths: rest } : {} });
12348
- 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
+ }
12349
12251
  }
12350
- async function down(args, dependencies) {
12351
- const context = await getContext(args, dependencies);
12352
- await destroyDeployment(context);
12353
- await destroyWorkspace(context, `${context.state.sandboxId}:workspace-destroy`).catch(() => {
12354
- return;
12355
- });
12356
- await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes/${context.state.sandboxId}`, { method: "DELETE" }).catch(() => {
12357
- return;
12358
- });
12359
- await rm3(context.file, { force: true });
12360
- console.log(`sandbox ${context.state.sandboxId} destroyed`);
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;
12361
12260
  }
12362
- async function resolveAssignedTargetHost(context, fallback) {
12363
- if (fallback?.trim())
12364
- return fallback.trim();
12365
- const body = await request(context.apiUrl, context.apiKey, "/v1/hosts");
12366
- const host = Array.isArray(body.hosts) ? body.hosts.find((candidate) => candidate?.id === context.state.hostId) : undefined;
12367
- const targetHost = host?.labels?.targetHost;
12368
- return typeof targetHost === "string" && targetHost.trim() ? targetHost.trim() : undefined;
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
+ };
12369
12281
  }
12370
- async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
12371
- const deploymentId = context.state.deploymentId;
12372
- if (!deploymentId)
12373
- throw new Error("deployment ID is required when recording sandbox routes");
12374
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
12375
- method: "POST",
12376
- body: JSON.stringify({
12377
- id: context.state.sandboxId,
12378
- goal: preview.goal,
12379
- status: status3,
12380
- stack: loaded.manifest.name ?? context.state.repository,
12381
- deploymentType: preview.deploymentType,
12382
- revisionId: deploymentId,
12383
- generation: Date.now(),
12384
- baseDomain: preview.baseDomain,
12385
- domains: await managedServiceDomains(context, loaded.manifest.name ?? context.state.repository, preview, deploymentId),
12386
- stableDomains: preview.stableDomains,
12387
- serviceRoutes,
12388
- healthEvidence,
12389
- images,
12390
- volumes: [
12391
- {
12392
- name: `${context.state.repository}-workspace`,
12393
- target: "/workspace",
12394
- access: "read-write"
12395
- }
12396
- ],
12397
- repositories: [context.state.repository],
12398
- executor: "sandblocks"
12399
- })
12400
- });
12401
- return body.sandbox;
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);
12402
12286
  }
12403
- async function createEnvironmentSnapshot(context) {
12404
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots`, {
12405
- method: "POST",
12406
- body: JSON.stringify({
12407
- stack: context.state.repository,
12408
- app: "*",
12409
- environmentId: context.state.environment
12410
- })
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
12296
+ });
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 } : {} };
12411
12305
  });
12412
- const id2 = body.snapshot?.id;
12413
- if (typeof id2 !== "string" || !/^[a-f0-9-]{36}$/.test(id2)) {
12414
- throw new Error("managed environment snapshot response is invalid");
12415
- }
12416
- return id2;
12417
12306
  }
12418
- async function releaseEnvironmentSnapshot(context, snapshotId) {
12419
- await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
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}`);
12420
12311
  }
12421
- async function destroyDeployment(context) {
12422
- if (!context.state.deploymentId)
12423
- return;
12424
- await destroyDeploymentById(context, context.state.deploymentId);
12425
- const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...remainingState } = context.state;
12426
- context.state = remainingState;
12427
- await writeState(context.file, context.state);
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}`);
12322
+ }
12428
12323
  }
12429
- async function destroyDeploymentById(context, deploymentId) {
12430
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
12431
- method: "POST",
12432
- headers: { "idempotency-key": `preview:${deploymentId}:destroy:${Date.now()}` },
12433
- body: JSON.stringify({
12434
- kind: "workspace.service.destroy",
12435
- payload: { deploymentId, placement: { hostId: context.state.hostId } },
12436
- maxAttempts: 1
12437
- })
12438
- });
12439
- 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
+ }
12440
12334
  }
12441
- async function destroyWorkspace(context, idempotencyKey) {
12442
- const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
12443
- method: "DELETE",
12444
- headers: {
12445
- "content-type": "application/json",
12446
- "x-sandblocks-api-key": context.apiKey,
12447
- "idempotency-key": idempotencyKey
12448
- },
12449
- body: JSON.stringify({ idempotencyKey })
12450
- });
12451
- const body = await response.json().catch(() => ({}));
12452
- if (response.ok)
12453
- await wait(context.apiUrl, context.apiKey, body.operation.id);
12454
- else if (![404, 409].includes(response.status))
12455
- 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];
12456
12343
  }
12457
- async function importSource(input) {
12458
- const source = await input.source(input.root);
12459
- const response = await fetchWithRetry(`${input.apiUrl}/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/import`, {
12460
- method: "POST",
12461
- headers: {
12462
- "content-type": "application/x-tar",
12463
- "content-length": String(source.bundle.byteLength),
12464
- "x-sandblocks-api-key": input.apiKey,
12465
- "x-sandblocks-workspace-id": input.workspaceId,
12466
- "x-sandblocks-sandbox-id": input.sandboxId,
12467
- "x-sandblocks-worker-pool": input.pool,
12468
- ...input.workspaceImage ? { "x-sandblocks-workspace-image": input.workspaceImage } : {},
12469
- "idempotency-key": input.idempotencyKey
12470
- },
12471
- body: new Blob([Uint8Array.from(source.bundle)])
12472
- });
12473
- const submitted = await response.json().catch(() => ({}));
12474
- if (!response.ok)
12475
- throw new Error(String(submitted?.error ?? `sandbox import failed (${response.status})`));
12476
- return {
12477
- files: source.files,
12478
- bytes: source.bundle.byteLength,
12479
- 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 {}
12480
12360
  };
12481
12361
  }
12482
- async function dataOperation(context, action, body) {
12483
- context.state.sequence += 1;
12484
- await writeState(context.file, context.state);
12485
- const submitted = await leaseRequest(context, `/v1/sandboxes/${context.state.sandboxId}/workspaces/${context.state.repository}/${action}`, {
12486
- method: "POST",
12487
- headers: { "idempotency-key": `cli:${context.state.sandboxId}:${context.state.sequence}:${action}` },
12488
- body: JSON.stringify(body)
12489
- });
12490
- return waitLease(context, submitted.operation.id);
12491
- }
12492
- async function getContext(args, dependencies) {
12493
- const options = dependencies.parse(args);
12494
- const root = path5.resolve(options.positionals[0] ?? process.cwd());
12495
- await loadLocalEnvironment(root);
12496
- const environment = dependencies.option(options, "environment") ?? "preview";
12497
- const file = statePath(root, environment);
12498
- let state;
12362
+ async function withTimeout(promise, timeoutMs, label) {
12363
+ let timer;
12499
12364
  try {
12500
- state = JSON.parse(await readFile4(file, "utf8"));
12501
- } catch {
12502
- 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);
12503
12374
  }
12504
- const configured = client(options, dependencies.option, state.projectId);
12505
- return { root, file, options, state, ...configured };
12506
- }
12507
- function selectPreview(loaded, id2) {
12508
- return resolveEnvironment(loaded.manifest, id2);
12509
- }
12510
- function client(options, option2, projectId) {
12511
- const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
12512
- const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY ?? "";
12513
- if (!apiUrl || !apiKey)
12514
- throw new Error("SANDBLOCKS_API_URL and SANDBLOCKS_API_KEY are required");
12515
- return { apiUrl, apiKey, projectId };
12516
12375
  }
12517
- async function request(apiUrl, apiKey, pathname, init = {}) {
12518
- const headers = new Headers(init.headers);
12519
- headers.set("x-sandblocks-api-key", apiKey);
12520
- if (init.body)
12521
- headers.set("content-type", "application/json");
12522
- const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
12523
- const body = await response.json().catch(() => ({}));
12524
- if (!response.ok)
12525
- throw new Error(String(body.error ?? `Sandblocks request failed (${response.status})`));
12526
- return body;
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`);
12527
12386
  }
12528
- async function leaseRequest(context, pathname, init = {}) {
12529
- const headers = new Headers(init.headers);
12530
- headers.set("authorization", `Bearer ${context.state.leaseToken}`);
12531
- if (init.body)
12532
- headers.set("content-type", "application/json");
12533
- const response = await fetchWithRetry(`${context.apiUrl}${pathname}`, { ...init, headers });
12534
- const body = await response.json().catch(() => ({}));
12535
- if (!response.ok)
12536
- throw new Error(String(body.error ?? `Sandblocks lease request failed (${response.status})`));
12537
- return body;
12387
+ function array(value) {
12388
+ return Array.isArray(value) ? value : [value];
12538
12389
  }
12539
- async function waitForPublishedRoutes(routes, timeoutMs = 90000) {
12540
- const deadline = Date.now() + timeoutMs;
12541
- let pending = routes.map((route) => ({
12542
- label: route.url,
12543
- url: route.service === "api" ? new URL("/health", route.url).toString() : route.url
12544
- }));
12545
- while (pending.length && Date.now() < deadline) {
12546
- const checks = await Promise.all(pending.map(async (route) => {
12547
- try {
12548
- const response = await fetch(route.url, { redirect: "manual" });
12549
- await response.body?.cancel().catch(() => {
12550
- return;
12551
- });
12552
- return response.status >= 200 && response.status < 400 ? undefined : route;
12553
- } catch {
12554
- return route;
12555
- }
12556
- }));
12557
- pending = checks.filter((route) => typeof route === "object" && route !== null);
12558
- if (pending.length)
12559
- await Bun.sleep(2000);
12560
- }
12561
- if (pending.length) {
12562
- throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
12563
- }
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;
12564
12433
  }
12565
- async function fetchWithRetry(url, init = {}) {
12566
- const method = (init.method ?? "GET").toUpperCase();
12567
- const headers = new Headers(init.headers);
12568
- const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
12569
- const attempts = safeToRetry ? 8 : 1;
12570
- let lastError;
12571
- for (let attempt = 0;attempt < attempts; attempt += 1) {
12572
- try {
12573
- const response = await fetch(url, init);
12574
- if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
12575
- return response;
12576
- await response.body?.cancel().catch(() => {
12577
- return;
12578
- });
12579
- } catch (error) {
12580
- lastError = error;
12581
- if (attempt === attempts - 1)
12582
- throw error;
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;
12445
+ }
12446
+ listStacks(projectId) {
12447
+ return this.request(`/v1/projects/${encodeURIComponent(projectId)}/stacks`);
12448
+ }
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));
12583
12689
  }
12584
- await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
12690
+ throw new Error("Sandblocks operation timed out");
12585
12691
  }
12586
- throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
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;
12712
+ }
12713
+ }
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);
12727
+ }
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}'`);
12736
+ }
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);
12746
+ }
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}`);
12761
+ }
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}`);
12777
+ }
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"}`);
12789
+ }
12790
+ }
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);
12825
+ }
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;
12829
+ }
12830
+ async function gitPath(root, name) {
12831
+ return path3.resolve(root, await git(root, ["rev-parse", "--git-path", name]));
12587
12832
  }
12588
- async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
12589
- const deadline = Date.now() + timeoutMs;
12590
- while (Date.now() < deadline) {
12591
- const body = await request(apiUrl, apiKey, `/v1/operations/${operationId}`);
12592
- if (body.operation.state === "succeeded")
12593
- return body.operation;
12594
- if (["failed", "cancelled"].includes(body.operation.state))
12595
- throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
12596
- await Bun.sleep(500);
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();
12843
+ }
12844
+ async function exists(file) {
12845
+ return Bun.file(file).exists();
12846
+ }
12847
+ async function text(file) {
12848
+ try {
12849
+ return await readFile4(file, "utf8");
12850
+ } catch {
12851
+ return;
12597
12852
  }
12598
- throw new Error(`operation ${operationId} timed out`);
12599
12853
  }
12600
- async function waitLease(context, operationId) {
12601
- const deadline = Date.now() + 3 * 60000;
12602
- while (Date.now() < deadline) {
12603
- const body = await leaseRequest(context, `/v1/sandbox-operations/${operationId}`);
12604
- if (body.operation.state === "succeeded")
12605
- return body.operation;
12606
- if (["failed", "cancelled"].includes(body.operation.state))
12607
- throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
12608
- await Bun.sleep(500);
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");
12609
12880
  }
12610
- throw new Error(`sandbox operation ${operationId} timed out`);
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)}`);
12611
12891
  }
12612
- async function loadLocalEnvironment(root) {
12613
- const file = path5.join(root, ".sandblocks", "config.env");
12614
- let contents;
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
+ });
12903
+ }
12904
+ async function exists2(file) {
12615
12905
  try {
12616
- contents = await readFile4(file, "utf8");
12906
+ await access2(file);
12907
+ return true;
12617
12908
  } catch {
12618
- return;
12909
+ return false;
12619
12910
  }
12620
- for (const raw of contents.split(/\r?\n/)) {
12621
- const line = raw.trim();
12622
- if (!line || line.startsWith("#") || !line.includes("="))
12623
- continue;
12624
- const [key, ...parts] = line.split("=");
12625
- if (!key?.startsWith("SANDBLOCKS_") || process.env[key] !== undefined)
12911
+ }
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");
12937
+ }
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);
12626
12945
  continue;
12627
- process.env[key] = parts.join("=").trim().replace(/^['"]|['"]$/g, "");
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;
12628
13083
  }
12629
13084
  }
12630
- async function applyManagedRoutingPolicy(context, stack, preview) {
12631
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
12632
- const policy = body.policy;
12633
- const primaryDomains = Object.fromEntries(Object.entries(policy.domains ?? {}).flatMap(([service, domains]) => {
12634
- const domain = Array.isArray(domains) ? domains[0] : domains;
12635
- return domain ? [[service, domain]] : [];
12636
- }));
12637
- return {
12638
- ...preview,
12639
- baseDomain: policy.baseDomain ?? preview.baseDomain,
12640
- domains: { ...preview.domains, ...primaryDomains },
12641
- stableDomains: { ...preview.stableDomains, ...policy.stableDomains }
12642
- };
12643
- }
12644
- function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
12645
- return Object.fromEntries(preview.services.flatMap((service) => {
12646
- if (service.kind === "process" && !service.port)
12647
- return [];
12648
- const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
12649
- const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
12650
- const configuredStableHosts = preview.stableDomains[service.id];
12651
- const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.deploymentType}-${stack}-${service.id}.${preview.baseDomain}`;
12652
- return [
12653
- [`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
12654
- [`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
12655
- ];
12656
- }));
12657
- }
12658
- async function managedServiceDomains(context, stack, preview, deploymentId) {
12659
- const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
12660
- const domains = body.policy.domains ?? {};
12661
- return Object.keys(domains).length ? domains : resolvedServiceDomains(context.state.sandboxId, deploymentId, preview);
12662
- }
12663
- function resolvedServiceDomains(sandboxId, deploymentId, preview) {
12664
- return Object.fromEntries(preview.services.map((service) => [
12665
- service.id,
12666
- resolvedServiceDomain(sandboxId, deploymentId, service.id, preview)
12667
- ]));
12668
- }
12669
- function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
12670
- const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${deploymentId}/${service}`).digest("hex").slice(0, 8)}`;
12671
- const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
12672
- return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
12673
- }
12674
- function statePath(root, environment) {
12675
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(environment))
12676
- throw new Error("sandbox environment is invalid");
12677
- return path5.join(root, ".sandblocks", `sandbox-${environment}.json`);
12678
- }
12679
- async function writeState(file, state) {
12680
- await mkdir3(path5.dirname(file), { recursive: true, mode: 448 });
12681
- await chmod3(path5.dirname(file), 448);
12682
- const temporary = `${file}.${process.pid}.tmp`;
12683
- await Bun.write(temporary, `${JSON.stringify(state, null, 2)}
12684
- `, { mode: 384 });
12685
- await chmod3(temporary, 384);
12686
- await rename(temporary, file);
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}`);
12687
13102
  }
12688
- function redactedState(state) {
12689
- return { ...state, leaseToken: "<redacted>" };
13103
+ async function deploy(args, dependencies) {
13104
+ await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
12690
13105
  }
12691
- function printOutput(operation) {
12692
- if (operation.result?.stdout)
12693
- process.stdout.write(String(operation.result.stdout));
12694
- if (operation.result?.stderr)
12695
- process.stderr.write(String(operation.result.stderr));
12696
- if (Number(operation.result?.exitCode ?? 0) !== 0)
12697
- throw new Error(`command exited ${operation.result.exitCode}`);
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
+ }
12698
13125
  }
12699
-
12700
- // src/sdk.ts
12701
- import { mkdir as mkdir5, readFile as readFile7, rm as rm6, writeFile as writeFile4 } from "fs/promises";
12702
- import { dirname as dirname2, resolve as resolve7 } from "path";
12703
-
12704
- // ../sdk/dist/index.js
12705
- import { randomUUID as randomUUID2 } from "crypto";
12706
- import { appendFile, cp, mkdir as mkdir4, readFile as readFile5, rm as rm4 } from "fs/promises";
12707
- import { dirname, resolve } from "path";
12708
- import { spawn as spawn2 } from "child_process";
12709
- import { access as access2, mkdir as mkdir22, writeFile as writeFile2 } from "fs/promises";
12710
- import { resolve as resolve2 } from "path";
12711
-
12712
- class CommandAgent {
12713
- options;
12714
- id;
12715
- constructor(options) {
12716
- this.options = options;
12717
- this.id = options.id ?? "command";
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);
12718
13160
  }
12719
- async run(context) {
12720
- const startedAt = new Date().toISOString();
12721
- const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
12722
- const promptMode = this.options.prompt ?? "stdin";
12723
- const command = promptMode === "argument" ? [...base, context.prompt] : base;
12724
- const idle = new AbortController;
12725
- const completed = new AbortController;
12726
- let idleTimer;
12727
- let completionTimer;
12728
- const resetIdle = () => {
12729
- if (idleTimer)
12730
- clearTimeout(idleTimer);
12731
- idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
12732
- };
12733
- resetIdle();
12734
- let output = "";
12735
- let stdout = "";
12736
- let stderr = "";
12737
- let lineBuffer = "";
12738
- let completionSignal;
12739
- const consume = async (chunk, stream) => {
12740
- resetIdle();
12741
- output += chunk;
12742
- if (stream === "stdout")
12743
- stdout += chunk;
12744
- else
12745
- stderr += chunk;
12746
- lineBuffer += chunk;
12747
- const lines = lineBuffer.split(/\r?\n/);
12748
- lineBuffer = lines.pop() ?? "";
12749
- for (const line of lines)
12750
- await this.emitLine(line, stream, context);
12751
- completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
12752
- if (completionSignal && !completionTimer) {
12753
- completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
12754
- }
12755
- };
13161
+ if (!artifactImage && serviceSteps.length) {
13162
+ buildWorkspaceId ??= `${context.state.sandboxId.slice(0, 8)}-${context.state.repository}-build-${deploymentId.slice(0, 8)}`.slice(0, 63);
12756
13163
  try {
12757
- const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
12758
- try {
12759
- const result = await context.runtime.exec(command, {
12760
- env: this.options.env,
12761
- stdin: promptMode === "stdin" ? context.prompt : undefined,
12762
- signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
12763
- onStdout: (chunk) => consume(chunk, "stdout"),
12764
- onStderr: (chunk) => consume(chunk, "stderr")
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 } : {}
12765
13175
  });
12766
- stdout = result.stdout;
12767
- stderr = result.stderr;
12768
- return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
12769
- } catch (error) {
12770
- if (!completionSignal)
12771
- throw error;
12772
- return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
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);
12773
13211
  }
12774
- } finally {
12775
- if (idleTimer)
12776
- clearTimeout(idleTimer);
12777
- if (completionTimer)
12778
- clearTimeout(completionTimer);
12779
- if (lineBuffer)
12780
- await this.emitLine(lineBuffer, "stdout", context);
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`" : ""}`);
12781
13255
  }
12782
13256
  }
12783
- result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
12784
- const sessionId = this.options.sessionId?.(stdout, stderr);
12785
- return {
12786
- iteration: context.iteration,
12787
- exitCode,
12788
- stdout,
12789
- stderr,
12790
- ...sessionId ? { sessionId } : {},
12791
- ...completionSignal ? { completionSignal } : {},
12792
- startedAt,
12793
- finishedAt: new Date().toISOString()
12794
- };
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 }) });
12795
13324
  }
12796
- async emitLine(line, stream, context) {
12797
- const timestamp = new Date().toISOString();
12798
- const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
12799
- const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
12800
- {
12801
- type: "raw",
12802
- line: stream === "stderr" ? `[stderr] ${line}` : line,
12803
- iteration: context.iteration,
12804
- timestamp
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)}`);
12805
13363
  }
12806
- ];
12807
- for (const event of events)
12808
- await context.emit(event);
12809
- }
12810
- }
12811
-
12812
- class ClaudeCodeAgent extends CommandAgent {
12813
- constructor(options) {
12814
- super({
12815
- id: "claude-code",
12816
- env: credentialEnvironment(options.env, options.credential),
12817
- prompt: "argument",
12818
- command: (context) => [
12819
- options.binary ?? "claude",
12820
- "--print",
12821
- "--model",
12822
- options.model,
12823
- ...options.effort ? ["--effort", options.effort] : [],
12824
- ...options.extraArgs ?? []
12825
- ],
12826
- sessionId: extractSessionId,
12827
- parseEvent: parseJsonAgentEvent
13364
+ sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
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;
12828
13376
  });
13377
+ throw error;
12829
13378
  }
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}`);
12830
13390
  }
12831
-
12832
- class CodexAgent extends CommandAgent {
12833
- constructor(options) {
12834
- super({
12835
- id: "codex",
12836
- env: credentialEnvironment(options.env, options.credential),
12837
- prompt: "argument",
12838
- command: (context) => [
12839
- options.binary ?? "codex",
12840
- "exec",
12841
- "--model",
12842
- options.model,
12843
- ...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
12844
- ...options.extraArgs ?? []
12845
- ],
12846
- sessionId: extractSessionId,
12847
- 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 } : {}
12848
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;
12849
13437
  }
12850
13438
  }
12851
-
12852
- class PiAgent extends CommandAgent {
12853
- constructor(options) {
12854
- super({
12855
- id: "pi",
12856
- env: credentialEnvironment(options.env, options.credential),
12857
- prompt: "argument",
12858
- command: (context) => [
12859
- options.binary ?? "pi",
12860
- "--print",
12861
- "--model",
12862
- options.model,
12863
- ...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
12864
- ...options.extraArgs ?? []
12865
- ],
12866
- sessionId: extractSessionId,
12867
- 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;
12868
13460
  });
12869
13461
  }
13462
+ return lease;
12870
13463
  }
12871
- function parseJsonAgentEvent(line, context) {
12872
- let value;
12873
- try {
12874
- value = JSON.parse(line);
12875
- } catch {
12876
- return { type: "raw", line, ...context };
12877
- }
12878
- const type = String(value.type ?? "");
12879
- if (/tool/i.test(type)) {
12880
- return {
12881
- type: "toolCall",
12882
- name: String(value.name ?? value.tool ?? value.tool_name ?? "tool"),
12883
- ...value.input !== undefined ? { input: value.input } : {},
12884
- ...context
12885
- };
12886
- }
12887
- const text2 = value.text ?? value.message ?? value.content;
12888
- return typeof text2 === "string" ? { type: "text", text: text2, ...context } : { type: "raw", line, ...context };
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
+ }
13543
+ ],
13544
+ repositories: [context.state.repository],
13545
+ executor: "sandblocks"
13546
+ })
13547
+ });
13548
+ return body.sandbox;
12889
13549
  }
12890
- function credentialEnvironment(env, credential) {
12891
- if (!credential)
12892
- return env;
12893
- if (!/^[A-Z][A-Z0-9_]*$/.test(credential.environment))
12894
- throw new Error("credential environment name is invalid");
12895
- const value = process.env[credential.environment];
12896
- if (value === undefined)
12897
- return env;
12898
- const target = credential.target ?? credential.environment;
12899
- if (!/^[A-Z][A-Z0-9_]*$/.test(target))
12900
- throw new Error("credential target name is invalid");
12901
- return { ...env, [target]: value };
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");
13562
+ }
13563
+ return id2;
12902
13564
  }
12903
- function extractSessionId(stdout, stderr) {
12904
- const match = `${stdout}
12905
- ${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
12906
- 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" });
12907
13567
  }
12908
- var Output = {
12909
- string(input) {
12910
- return { kind: "string", tag: validTag(input.tag) };
12911
- },
12912
- object(input) {
12913
- return { kind: "object", tag: validTag(input.tag), schema: input.schema };
12914
- }
12915
- };
12916
- async function extractOutput(spec, text2) {
12917
- const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
12918
- const match = text2.match(pattern);
12919
- if (!match)
12920
- throw new Error(`structured output tag <${spec.tag}> was not found`);
12921
- const content = match[1]?.trim();
12922
- if (content === undefined)
12923
- throw new Error(`structured output tag <${spec.tag}> is empty`);
12924
- if (spec.kind === "string")
12925
- return content;
12926
- let value;
12927
- try {
12928
- value = JSON.parse(content);
12929
- } catch {
12930
- throw new Error(`structured output <${spec.tag}> is not valid JSON`);
12931
- }
12932
- const schema = spec.schema;
12933
- if (schema["~standard"]) {
12934
- const result = await schema["~standard"].validate(value);
12935
- if (result.issues?.length)
12936
- throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
12937
- return result.value;
12938
- }
12939
- if (schema.safeParse) {
12940
- const result = schema.safeParse(value);
12941
- if (!result.success)
12942
- throw new Error(`structured output validation failed: ${String(result.error)}`);
12943
- return result.data;
12944
- }
12945
- if (schema.parse)
12946
- return schema.parse(value);
12947
- 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);
12948
13575
  }
12949
- function validTag(tag) {
12950
- if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tag))
12951
- throw new Error("output tag is invalid");
12952
- 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);
12953
13590
  }
12954
- function escapeRegex(value) {
12955
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13591
+ async function destroyWorkspace(context, idempotencyKey) {
13592
+ await destroyDetachedWorkspace(context, context.state.workspaceId, idempotencyKey);
12956
13593
  }
12957
- async function executeProcess(command, options = {}) {
12958
- if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
12959
- throw new Error("command must contain safe non-empty arguments");
12960
- }
12961
- const startedAt = new Date().toISOString();
12962
- const [executable, ...args] = command;
12963
- if (!executable)
12964
- throw new Error("command executable is required");
12965
- const child = spawn2(executable, args, {
12966
- cwd: options.cwd ?? options.hostCwd,
12967
- env: options.env ? { ...process.env, ...options.env } : process.env,
12968
- 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 })
12969
13603
  });
12970
- if (options.stdin !== undefined)
12971
- child.stdin?.end(options.stdin);
12972
- let timedOut = false;
12973
- let stdout = "";
12974
- let stderr = "";
12975
- let callbackQueue = Promise.resolve();
12976
- child.stdout?.on("data", (chunk) => {
12977
- const text2 = chunk.toString();
12978
- stdout = `${stdout}${text2}`.slice(-4 * 1024 * 1024);
12979
- callbackQueue = callbackQueue.then(() => options.onStdout?.(text2)).then(() => {
12980
- return;
12981
- });
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)])
12982
13625
  });
12983
- child.stderr?.on("data", (chunk) => {
12984
- const text2 = chunk.toString();
12985
- stderr = `${stderr}${text2}`.slice(-4 * 1024 * 1024);
12986
- callbackQueue = callbackQueue.then(() => options.onStderr?.(text2)).then(() => {
12987
- return;
12988
- });
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)
12989
13642
  });
12990
- const stop = () => child.kill("SIGTERM");
12991
- options.signal?.addEventListener("abort", stop, { once: true });
12992
- const timer = options.timeoutMs ? setTimeout(() => {
12993
- timedOut = true;
12994
- child.kill("SIGKILL");
12995
- }, 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;
12996
13652
  try {
12997
- const exitCode = await new Promise((resolve3, reject) => {
12998
- child.once("error", reject);
12999
- child.once("close", (code, signal) => resolve3(code ?? (signal ? 128 : 1)));
13000
- });
13001
- await callbackQueue;
13002
- if (options.signal?.aborted)
13003
- throw options.signal.reason ?? new Error("operation aborted");
13004
- return {
13005
- exitCode: timedOut ? 124 : exitCode,
13006
- stdout,
13007
- stderr,
13008
- timedOut,
13009
- startedAt,
13010
- finishedAt: new Date().toISOString()
13011
- };
13012
- } finally {
13013
- if (timer)
13014
- clearTimeout(timer);
13015
- 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`);
13016
13656
  }
13657
+ const configured = client(options, dependencies.option, state.projectId);
13658
+ return { root, file, options, state, ...configured };
13017
13659
  }
13018
- class SandblocksSandbox {
13019
- runtime;
13020
- options;
13021
- git;
13022
- id;
13023
- provider;
13024
- cwd;
13025
- failed = false;
13026
- disposed = false;
13027
- eventQueue = [];
13028
- eventWaiters = [];
13029
- constructor(runtime, options, git3) {
13030
- this.runtime = runtime;
13031
- this.options = options;
13032
- this.git = git3;
13033
- this.id = runtime.id;
13034
- this.provider = runtime.provider;
13035
- this.cwd = runtime.cwd;
13036
- }
13037
- exec(command, options = {}) {
13038
- return this.runtime.exec(command, options);
13039
- }
13040
- upload(source, destination) {
13041
- if (!this.runtime.upload)
13042
- throw new Error(`${this.provider} provider does not support upload`);
13043
- return this.runtime.upload(source, destination);
13044
- }
13045
- download(source, destination) {
13046
- if (!this.runtime.download)
13047
- throw new Error(`${this.provider} provider does not support download`);
13048
- 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);
13049
13713
  }
13050
- stop(reason) {
13051
- 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(", ")}`);
13052
13716
  }
13053
- async* events(options = {}) {
13054
- while (!this.disposed || this.eventQueue.length) {
13055
- 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(() => {
13056
13730
  return;
13057
- const queued = this.eventQueue.shift();
13058
- if (queued) {
13059
- yield queued;
13060
- continue;
13061
- }
13062
- const event = await new Promise((resolve22) => {
13063
- const abort = () => resolve22(undefined);
13064
- options.signal?.addEventListener("abort", abort, { once: true });
13065
- this.eventWaiters.push((value) => {
13066
- options.signal?.removeEventListener("abort", abort);
13067
- resolve22(value);
13068
- });
13069
13731
  });
13070
- if (!event)
13071
- return;
13072
- yield event;
13073
- }
13074
- }
13075
- async run(options) {
13076
- if (options.output && (options.maxIterations ?? 1) !== 1)
13077
- throw new Error("structured output requires maxIterations === 1");
13078
- const startedAt = new Date().toISOString();
13079
- const prompt = await resolvePrompt(options);
13080
- const signals = array(options.completionSignal ?? "<promise>COMPLETE</promise>");
13081
- const logger = createLogger(options.logging, this.git.hostCwd);
13082
- const baseline = await gitHead(this.git.runtimeCwd);
13083
- const iterations = [];
13084
- try {
13085
- await runSandboxHooks(this.runtime, options.hooks?.sandbox?.beforeAgent);
13086
- for (let iteration = 1;iteration <= (options.maxIterations ?? 1); iteration += 1) {
13087
- const result = await options.agent.run({
13088
- runtime: this.runtime,
13089
- prompt,
13090
- iteration,
13091
- signal: options.signal,
13092
- idleTimeoutSeconds: options.idleTimeoutSeconds ?? 600,
13093
- completionTimeoutSeconds: options.completionTimeoutSeconds ?? 60,
13094
- completionSignal: signals,
13095
- emit: async (event) => {
13096
- this.publish(event);
13097
- await logger(event);
13098
- }
13099
- });
13100
- iterations.push(result);
13101
- if (result.exitCode !== 0)
13102
- throw new Error(result.stderr || `${options.agent.id} exited ${result.exitCode}`);
13103
- if (result.completionSignal)
13104
- break;
13105
- }
13106
- await runSandboxHooks(this.runtime, options.hooks?.sandbox?.afterAgent);
13107
- await this.runtime.syncToHost?.();
13108
- const commits = await collectCommits(this.git.runtimeCwd, baseline, this.options.timeouts?.commitCollectionMs);
13109
- if (this.git.mergeToHead && this.git.branch)
13110
- await mergeBranch(this.git, this.options.timeouts?.mergeToHostMs);
13111
- const allOutput = iterations.map((item) => item.stdout).join(`
13112
- `);
13113
- const completionSignal = iterations.find((item) => item.completionSignal)?.completionSignal;
13114
- return {
13115
- sandboxId: this.id,
13116
- provider: this.provider,
13117
- iterations,
13118
- commits,
13119
- ...this.git.branch ? { branch: this.git.branch } : {},
13120
- ...completionSignal ? { completionSignal } : {},
13121
- ...options.output ? { output: await extractOutput(options.output, allOutput) } : {},
13122
- startedAt,
13123
- finishedAt: new Date().toISOString()
13124
- };
13125
13732
  } catch (error) {
13126
- this.failed = true;
13127
- throw error;
13128
- }
13129
- }
13130
- async destroy() {
13131
- if (this.disposed)
13132
- return;
13133
- this.disposed = true;
13134
- await runHostHooks(this.options.hooks?.host?.beforeDestroy, this.git.hostCwd);
13135
- const retention = this.options.retention ?? "destroy";
13136
- if (retention === "destroy" || retention === "on-failure" && !this.failed)
13137
- await this.runtime.destroy();
13138
- for (const waiter of this.eventWaiters.splice(0))
13139
- waiter(undefined);
13140
- if (this.git.worktree && retention !== "always") {
13141
- await executeProcess([
13142
- "git",
13143
- "-C",
13144
- this.git.hostCwd,
13145
- "worktree",
13146
- "remove",
13147
- "--force",
13148
- this.git.worktree
13149
- ]);
13150
- await rm4(this.git.worktree, { recursive: true, force: true });
13733
+ lastError = error;
13734
+ if (attempt === attempts - 1)
13735
+ throw error;
13151
13736
  }
13737
+ await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
13152
13738
  }
13153
- async[Symbol.asyncDispose]() {
13154
- await this.destroy();
13155
- }
13156
- publish(event) {
13157
- const waiter = this.eventWaiters.shift();
13158
- if (waiter)
13159
- waiter(event);
13160
- else
13161
- this.eventQueue.push(event);
13162
- }
13739
+ throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
13163
13740
  }
13164
- async function createSandbox(options) {
13165
- const provider = options.provider ?? options.sandbox;
13166
- if (!provider)
13167
- throw new Error("sandbox provider is required");
13168
- assertProviderCapabilities(provider, options);
13169
- const hostCwd = resolve(options.cwd ?? process.cwd());
13170
- await runHostHooks(options.hooks?.host?.beforeCreate, hostCwd);
13171
- const branchStrategy = options.branch ?? options.branchStrategy ?? { type: "head" };
13172
- if (options.copyToWorktree?.length && branchStrategy.type === "head") {
13173
- throw new Error("copyToWorktree is not supported with the head branch strategy");
13174
- }
13175
- const git3 = await prepareGit(hostCwd, branchStrategy, options.timeouts?.gitSetupMs);
13176
- await runHostHooks(options.hooks?.host?.onWorktreeReady, git3.runtimeCwd);
13177
- for (const file of options.copyToWorktree ?? []) {
13178
- const source = resolve(hostCwd, file);
13179
- const destination = resolve(git3.runtimeCwd, file);
13180
- await mkdir4(dirname(destination), { recursive: true });
13181
- await withTimeout(cp(source, destination, { recursive: true }), options.timeouts?.copyToWorktreeMs ?? 60000, `copying '${file}'`);
13182
- }
13183
- const id2 = options.id ?? randomUUID2();
13184
- const runtime = await provider.create({
13185
- id: id2,
13186
- cwd: git3.runtimeCwd,
13187
- image: options.image,
13188
- env: options.env,
13189
- mounts: options.mounts,
13190
- network: options.network,
13191
- groups: options.groups,
13192
- devices: options.devices,
13193
- cpus: options.cpus,
13194
- memoryMb: options.memoryMb,
13195
- pids: options.pids,
13196
- tmpfsMb: options.tmpfsMb,
13197
- signal: options.signal,
13198
- idempotencyKey: options.idempotencyKey,
13199
- metadata: options.metadata
13200
- });
13201
- const sandbox = new SandblocksSandbox(runtime, options, git3);
13202
- try {
13203
- await runHostHooks(options.hooks?.host?.onSandboxReady, hostCwd);
13204
- await runHostHooks(options.hooks?.host?.afterCreate, hostCwd);
13205
- await runSandboxHooks(runtime, options.hooks?.sandbox?.onSandboxReady);
13206
- await runSandboxHooks(runtime, options.hooks?.sandbox?.afterCreate);
13207
- return sandbox;
13208
- } catch (error) {
13209
- await sandbox.destroy().catch(() => {
13210
- return;
13211
- });
13212
- 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);
13213
13750
  }
13751
+ throw new Error(`operation ${operationId} timed out`);
13214
13752
  }
13215
- async function connectSandbox(options) {
13216
- if (!options.provider.reconnect)
13217
- throw new Error(`${options.provider.kind} provider does not support reconnect`);
13218
- const cwd = resolve(options.cwd ?? process.cwd());
13219
- const runtime = await options.provider.reconnect({ id: options.id, cwd });
13220
- 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`);
13221
13764
  }
13222
- async function run2(options) {
13223
- const sandbox = await createSandbox(options);
13765
+ async function loadLocalEnvironment(root) {
13766
+ const file = path5.join(root, ".sandblocks", "config.env");
13767
+ let contents;
13224
13768
  try {
13225
- return await sandbox.run(options);
13226
- } finally {
13227
- await sandbox.destroy();
13769
+ contents = await readFile5(file, "utf8");
13770
+ } catch {
13771
+ return;
13772
+ }
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, "");
13228
13781
  }
13229
13782
  }
13230
- async function resolvePrompt(options) {
13231
- if (Boolean(options.prompt) === Boolean(options.promptFile))
13232
- throw new Error("provide exactly one of prompt or promptFile");
13233
- const promptFile = options.promptFile;
13234
- let prompt = options.prompt ?? await readFile5(resolve(process.cwd(), promptFile ?? ""), "utf8");
13235
- for (const [key, value] of Object.entries(options.promptArgs ?? {}))
13236
- prompt = prompt.replaceAll(`{{${key}}}`, value);
13237
- return prompt;
13238
- }
13239
- async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
13240
- if (strategy.type === "head")
13241
- return { hostCwd, runtimeCwd: hostCwd };
13242
- const originalHead = await gitHead(hostCwd);
13243
- if (strategy.type === "branch") {
13244
- await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
13245
- return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
13246
- }
13247
- const branch = strategy.name ?? `sandblocks/${randomUUID2().slice(0, 8)}`;
13248
- const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
13249
- await mkdir4(dirname(worktree), { recursive: true });
13250
- await checked(["git", "-C", hostCwd, "worktree", "add", "-B", branch, worktree, "HEAD"], timeoutMs);
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
+ }));
13251
13790
  return {
13252
- hostCwd,
13253
- runtimeCwd: worktree,
13254
- branch,
13255
- worktree,
13256
- mergeToHead: strategy.type === "merge-to-head",
13257
- originalHead
13791
+ ...preview,
13792
+ baseDomain: policy.baseDomain ?? preview.baseDomain,
13793
+ domains: { ...preview.domains, ...primaryDomains },
13794
+ stableDomains: { ...preview.stableDomains, ...policy.stableDomains }
13258
13795
  };
13259
13796
  }
13260
- async function mergeBranch(git3, timeoutMs = 60000) {
13261
- if (!git3.branch)
13262
- throw new Error("merge branch is unavailable");
13263
- await checked(["git", "-C", git3.hostCwd, "merge", "--no-edit", git3.branch], timeoutMs);
13264
- }
13265
- async function gitHead(cwd) {
13266
- const result = await executeProcess(["git", "-C", cwd, "rev-parse", "HEAD"]);
13267
- return result.exitCode === 0 ? result.stdout.trim() : "";
13268
- }
13269
- async function collectCommits(cwd, baseline, timeoutMs = 30000) {
13270
- if (!baseline)
13271
- return [];
13272
- const result = await executeProcess(["git", "-C", cwd, "log", "--format=%H%x00%s", `${baseline}..HEAD`], {
13273
- timeoutMs
13274
- });
13275
- if (result.exitCode !== 0 || !result.stdout.trim())
13276
- return [];
13277
- return result.stdout.trim().split(`
13278
- `).map((line) => {
13279
- const [sha, message] = line.split("\x00");
13280
- if (!sha)
13281
- throw new Error("Git returned an invalid commit record");
13282
- return { sha, ...message ? { message } : {} };
13283
- });
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
+ }));
13284
13810
  }
13285
- async function checked(command, timeoutMs) {
13286
- const result = await executeProcess(command, { timeoutMs });
13287
- if (result.exitCode !== 0)
13288
- 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);
13289
13815
  }
13290
- async function runHostHooks(hooks, cwd) {
13291
- for (const hook of hooks ?? []) {
13292
- const command = hookCommand(hook);
13293
- const result = await executeProcess(command, {
13294
- cwd: resolve(cwd, hook.cwd ?? "."),
13295
- env: hook.env,
13296
- timeoutMs: hook.timeoutMs
13297
- });
13298
- if (result.exitCode !== 0)
13299
- throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
13300
- }
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
+ ]));
13301
13821
  }
13302
- async function runSandboxHooks(runtime, hooks) {
13303
- for (const hook of hooks ?? []) {
13304
- const result = await runtime.exec(hookCommand(hook), {
13305
- cwd: hook.cwd,
13306
- env: hook.env,
13307
- timeoutMs: hook.timeoutMs
13308
- });
13309
- if (result.exitCode !== 0)
13310
- throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
13311
- }
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();
13312
13826
  }
13313
- function hookCommand(hook) {
13314
- if (Boolean(hook.command) === Boolean(hook.shell))
13315
- throw new Error("hook requires exactly one of command or shell");
13316
- if (hook.command)
13317
- return hook.command;
13318
- if (!hook.shell)
13319
- throw new Error("hook shell command is required");
13320
- 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);
13321
13835
  }
13322
- function createLogger(options, cwd) {
13323
- const config = options ?? { type: "file" };
13324
- const file = resolve(cwd, config.path ?? `.sandblocks/logs/${Date.now()}.log`);
13325
- return async (event) => {
13326
- const line = `${JSON.stringify(event)}
13327
- `;
13328
- if (config.type === "stdout")
13329
- process.stdout.write(event.type === "text" ? event.text : event.type === "raw" ? `${event.line}
13330
- ` : line);
13331
- if (config.type !== "stdout" && config.type !== "silent") {
13332
- await mkdir4(dirname(file), { recursive: true });
13333
- await appendFile(file, line);
13334
- }
13335
- try {
13336
- await config.onAgentStreamEvent?.(event);
13337
- } catch {}
13338
- };
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`);
13339
13840
  }
13340
- async function withTimeout(promise, timeoutMs, label) {
13341
- let timer;
13342
- try {
13343
- return await Promise.race([
13344
- promise,
13345
- new Promise((_, reject) => {
13346
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
13347
- })
13348
- ]);
13349
- } finally {
13350
- if (timer)
13351
- clearTimeout(timer);
13352
- }
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);
13353
13849
  }
13354
- function assertProviderCapabilities(provider, options) {
13355
- const capabilities = provider.capabilities;
13356
- if (options.mounts?.length && !capabilities.bindMounts)
13357
- throw new Error(`${provider.kind} provider does not support bind mounts`);
13358
- if (options.network && !capabilities.networks)
13359
- throw new Error(`${provider.kind} provider does not support network selection`);
13360
- if (options.devices?.length && !capabilities.devices)
13361
- throw new Error(`${provider.kind} provider does not support devices`);
13362
- if ((options.retention === "always" || options.retention === "on-failure") && !capabilities.persistent)
13363
- throw new Error(`${provider.kind} provider does not support retained sandboxes`);
13850
+ function redactedState(state) {
13851
+ return { ...state, leaseToken: "<redacted>" };
13364
13852
  }
13365
- function array(value) {
13366
- 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}`);
13367
13860
  }
13368
- async function initSdk(options = {}) {
13369
- const cwd = resolve2(options.cwd ?? process.cwd());
13370
- const root = resolve2(cwd, ".sandblocks");
13371
- const files = {
13372
- "main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
13373
- import { DockerProvider } from "@sandblocks/sdk/providers/docker";
13374
-
13375
- await using sandbox = await createSandbox({
13376
- provider: new DockerProvider({ image: "node:22-bookworm" }),
13377
- cwd: process.cwd(),
13378
- });
13379
-
13380
- const result = await sandbox.run({
13381
- agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
13382
- promptFile: ".sandblocks/prompts/implement.md",
13383
- logging: { type: "stdout" },
13384
- });
13385
-
13386
- console.log(result);
13387
- `,
13388
- "prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
13389
13861
 
13390
- <promise>COMPLETE</promise>
13391
- `,
13392
- ".env.example": `# Prefer Sandblocks managed secrets for remote providers.
13393
- CLAUDE_CODE_OAUTH_TOKEN=
13394
- # ANTHROPIC_API_KEY=
13395
- SANDBLOCKS_API_URL=
13396
- SANDBLOCKS_API_KEY=
13397
- `,
13398
- "config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
13399
- `
13400
- };
13401
- const written = [];
13402
- for (const [relative, content] of Object.entries(files)) {
13403
- const path6 = resolve2(root, relative);
13404
- await mkdir22(resolve2(path6, ".."), { recursive: true });
13405
- if (!options.force && await access2(path6).then(() => true).catch(() => false))
13406
- continue;
13407
- await writeFile2(path6, content, { mode: relative.includes(".env") ? 384 : 420 });
13408
- written.push(path6);
13409
- }
13410
- return written;
13411
- }
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";
13412
13865
 
13413
- // ../sdk/dist/providers/docker.js
13866
+ // packages/libs/sdk/dist/providers/docker.js
13414
13867
  import { realpath as realpath3 } from "fs/promises";
13415
13868
  import { resolve as resolve3 } from "path";
13416
13869
  import { spawn as spawn3 } from "child_process";
@@ -13656,7 +14109,7 @@ class DockerProvider extends OciProvider {
13656
14109
  }
13657
14110
  }
13658
14111
 
13659
- // ../sdk/dist/providers/podman.js
14112
+ // packages/libs/sdk/dist/providers/podman.js
13660
14113
  import { realpath as realpath4 } from "fs/promises";
13661
14114
  import { resolve as resolve5 } from "path";
13662
14115
  import { spawn as spawn4 } from "child_process";
@@ -13902,7 +14355,7 @@ class PodmanProvider extends OciProvider2 {
13902
14355
  }
13903
14356
  }
13904
14357
 
13905
- // ../sdk/dist/providers/remote.js
14358
+ // packages/libs/sdk/dist/providers/remote.js
13906
14359
  import { randomUUID as randomUUID3 } from "crypto";
13907
14360
  import { mkdtemp, readFile as readFile6, rm as rm5, stat as stat2, writeFile as writeFile3 } from "fs/promises";
13908
14361
  import { tmpdir } from "os";
@@ -14226,7 +14679,7 @@ async function sourceBundle(cwd) {
14226
14679
  }
14227
14680
  }
14228
14681
 
14229
- // ../sdk/dist/providers/unsafe-host.js
14682
+ // packages/libs/sdk/dist/providers/unsafe-host.js
14230
14683
  import { cp as cp2, realpath as realpath5 } from "fs/promises";
14231
14684
  import { resolve as resolve6 } from "path";
14232
14685
  import { spawn as spawn6 } from "child_process";
@@ -14335,7 +14788,7 @@ class UnsafeHostProvider {
14335
14788
  }
14336
14789
  }
14337
14790
 
14338
- // src/sdk.ts
14791
+ // packages/libs/cli/src/sdk.ts
14339
14792
  async function runSdkCommand(args, helpers) {
14340
14793
  const [command = "help", ...rest] = args;
14341
14794
  const parsed = helpers.parse(rest);
@@ -14351,7 +14804,7 @@ async function runSdkCommand(args, helpers) {
14351
14804
  const provider = providerFrom(options, helpers, cwd);
14352
14805
  const agent = agentFrom(options, helpers);
14353
14806
  const outputTag = helpers.option(options, "output-tag");
14354
- const result = await run2({
14807
+ const result = await run({
14355
14808
  provider,
14356
14809
  cwd,
14357
14810
  image: helpers.option(options, "image"),
@@ -14402,7 +14855,7 @@ async function sdkSandbox(args, helpers, defaultCwd) {
14402
14855
  hooks: hooksFrom(options),
14403
14856
  copyToWorktree: multi(options["copy-to-worktree"])
14404
14857
  });
14405
- await mkdir5(dirname2(stateFile), { recursive: true });
14858
+ await mkdir7(dirname2(stateFile), { recursive: true });
14406
14859
  const strategy = branchFrom(options, helpers);
14407
14860
  const worktree = strategy.type === "worktree" || strategy.type === "merge-to-head" ? sandbox2.cwd : undefined;
14408
14861
  await writeFile4(stateFile, JSON.stringify({
@@ -14649,14 +15102,14 @@ function parseMount(value) {
14649
15102
  return { source, target, readonly: mode === "ro" };
14650
15103
  }
14651
15104
 
14652
- // src/cli.ts
15105
+ // packages/libs/cli/src/cli.ts
14653
15106
  var HELP = `Sandblocks CLI
14654
15107
 
14655
15108
  sandblocks configure [directory] [--name <stack>] [--force]
14656
15109
  sandblocks validate [directory] [--config <path>] [--json]
14657
15110
  sandblocks register [directory] --project <id> [--repository-url <url>]
14658
15111
  [--api-url <url>] [--api-key <key>] [--json]
14659
- sandblocks doctor [directory] [--api-url <url>]
15112
+ sandblocks doctor [directory] [--api-url <url>] [--explain-placement] [--project <id>] [--json]
14660
15113
  sandblocks whoami [directory] [--api-url <url>] [--api-key <key>] [--json]
14661
15114
  sandblocks hooks <install|status|uninstall> [directory]
14662
15115
  sandblocks hooks run post-commit [directory]
@@ -14666,10 +15119,12 @@ var HELP = `Sandblocks CLI
14666
15119
  sandblocks app <list|get|settings|settings-set> --project <id> [--stack <id>] [--app <id>] [--file <json>] [--json]
14667
15120
  sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
14668
15121
  [--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
14669
- 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>]
14670
15124
  sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
14671
- sandblocks sandbox deploy [directory] [--environment <id>]
14672
- 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>]
14673
15128
  sandblocks sandbox status [directory] [--environment <id>] [--json]
14674
15129
  sandblocks sandbox promote --project <id> --sandbox <id> [--json]
14675
15130
  sandblocks sandbox agent <run|list|get|cancel|redeploy> --project <id> --sandbox <id>
@@ -14736,6 +15191,8 @@ async function run3(argv = process.argv.slice(2)) {
14736
15191
  await resource(command, args);
14737
15192
  else if (command === "secret")
14738
15193
  await secret(args);
15194
+ else if (command === "artifact")
15195
+ await artifact(args);
14739
15196
  else if (command === "sandbox")
14740
15197
  await sandbox(args);
14741
15198
  else if (command === "sdk")
@@ -14836,6 +15293,38 @@ async function doctor(args) {
14836
15293
  if (!response.ok)
14837
15294
  throw new Error(`API health failed (${response.status})`);
14838
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
+ }
14839
15328
  }
14840
15329
  const gitDetected = await stat3(path6.join(root, ".git")).then(() => true).catch(() => false);
14841
15330
  console.log(`git ${gitDetected ? "ok" : "not detected"}`);
@@ -14951,6 +15440,45 @@ async function secret(args) {
14951
15440
  }
14952
15441
  }
14953
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
+ }
14954
15482
  function printResources(kind, action, body) {
14955
15483
  if (action === "settings") {
14956
15484
  console.log(JSON.stringify(body.settings ?? {}, null, 2));
@@ -15120,6 +15648,51 @@ async function sandboxPromote(args) {
15120
15648
  console.log(`promoted ${sandboxId} to production`);
15121
15649
  }
15122
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
+ }
15123
15696
  async function createSourceTar(root) {
15124
15697
  const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
15125
15698
  cwd: root,
@@ -15430,4 +16003,4 @@ export {
15430
16003
  parse2 as parse
15431
16004
  };
15432
16005
 
15433
- //# debugId=95A280B04AAC415F64756E2164756E21
16006
+ //# debugId=E7F4E37BF2F49E8E64756E2164756E21