@sandblocks/cli 0.5.0-b.2 → 0.5.0-b.20
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 +2237 -1693
- package/dist/cli.js.map +15 -15
- package/dist/{runtime.js → runtime-bin.js} +7 -11
- package/package.json +3 -9
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
|
-
//
|
|
21
|
+
// packages/libs/manifest/src/project-graph.ts
|
|
22
22
|
function compileProjectGraph(manifest) {
|
|
23
23
|
return {
|
|
24
24
|
version: 2,
|
|
@@ -77,9 +77,9 @@ function compileProjectGraph(manifest) {
|
|
|
77
77
|
pipeline: environment.pipeline,
|
|
78
78
|
services: environment.services,
|
|
79
79
|
checks: environment.checks,
|
|
80
|
-
baseDomain:
|
|
81
|
-
domains:
|
|
82
|
-
stableDomains:
|
|
80
|
+
baseDomain: "sandblocks.dev",
|
|
81
|
+
domains: {},
|
|
82
|
+
stableDomains: {},
|
|
83
83
|
...environment.targetHost ? { targetHost: environment.targetHost } : {},
|
|
84
84
|
...typeof environment.routing.sso === "boolean" ? { ssoEnabled: environment.routing.sso } : {}
|
|
85
85
|
}))
|
|
@@ -89,7 +89,7 @@ function normalizeCommand(command) {
|
|
|
89
89
|
return typeof command === "string" ? ["sh", "-lc", command] : [...command];
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
569
|
+
// node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
|
|
570
570
|
var init_typeAliases = () => {};
|
|
571
571
|
|
|
572
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
4058
|
+
// packages/libs/manifest/src/schema.ts
|
|
4059
4059
|
function keyedCollection(schema) {
|
|
4060
4060
|
return exports_external.record(id, schema).transform((values) => Object.entries(values).map(([entryId, value]) => ({ id: entryId, ...value })));
|
|
4061
4061
|
}
|
|
@@ -4079,23 +4079,6 @@ function validateReferences(manifest, ctx) {
|
|
|
4079
4079
|
validateReferencesTo(ctx, environment.pipeline, taskIds, ["environments", index, "pipeline"], "task");
|
|
4080
4080
|
validateReferencesTo(ctx, environment.services, serviceIds, ["environments", index, "services"], "service");
|
|
4081
4081
|
validateReferencesTo(ctx, environment.checks, checkIds, ["environments", index, "checks"], "check");
|
|
4082
|
-
validateReferencesTo(ctx, Object.keys(environment.routing.domains), new Set(environment.services), ["environments", index, "routing", "domains"], "environment service");
|
|
4083
|
-
validateReferencesTo(ctx, Object.keys(environment.routing.stableDomains), new Set(environment.services), ["environments", index, "routing", "stableDomains"], "environment service");
|
|
4084
|
-
for (const [service, configured] of Object.entries({
|
|
4085
|
-
...environment.routing.domains,
|
|
4086
|
-
...environment.routing.stableDomains
|
|
4087
|
-
})) {
|
|
4088
|
-
for (const template of Array.isArray(configured) ? configured : [configured]) {
|
|
4089
|
-
const expanded = template.replaceAll(/\{(?:sandbox|revision|service)\}/g, "preview");
|
|
4090
|
-
if (expanded.includes("{") || !/^[a-z0-9.-]+$/.test(expanded.toLowerCase())) {
|
|
4091
|
-
ctx.addIssue({
|
|
4092
|
-
code: exports_external.ZodIssueCode.custom,
|
|
4093
|
-
path: ["environments", index, "routing", "domains", service],
|
|
4094
|
-
message: "domain template may use only {sandbox}, {revision}, and {service}"
|
|
4095
|
-
});
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
4082
|
}
|
|
4100
4083
|
}
|
|
4101
4084
|
function validateAcyclicServices(manifest, ctx) {
|
|
@@ -4269,12 +4252,7 @@ var init_schema = __esm(() => {
|
|
|
4269
4252
|
services: exports_external.array(id).min(1),
|
|
4270
4253
|
checks: exports_external.array(id).default([]),
|
|
4271
4254
|
targetHost: exports_external.string().min(1).max(255).optional(),
|
|
4272
|
-
routing: exports_external.object({
|
|
4273
|
-
baseDomain: exports_external.string().min(1).max(253).default("sandblocks.dev"),
|
|
4274
|
-
domains: exports_external.record(id, exports_external.string().min(1).max(253)).default({}),
|
|
4275
|
-
stableDomains: exports_external.record(id, exports_external.union([exports_external.string().min(1).max(253), exports_external.array(exports_external.string().min(1).max(253)).min(1)])).default({}),
|
|
4276
|
-
sso: exports_external.boolean().optional()
|
|
4277
|
-
}).strict().default({})
|
|
4255
|
+
routing: exports_external.object({ sso: exports_external.boolean().optional() }).strict().default({})
|
|
4278
4256
|
}).strict();
|
|
4279
4257
|
EnvironmentsV2Schema = keyedCollection(EnvironmentValueSchema);
|
|
4280
4258
|
HooksV2Schema = exports_external.object({
|
|
@@ -4302,7 +4280,7 @@ var init_schema = __esm(() => {
|
|
|
4302
4280
|
SandblocksManifestV2Schema = CompleteSandblocksManifestV2Schema.superRefine(validateReferences);
|
|
4303
4281
|
});
|
|
4304
4282
|
|
|
4305
|
-
//
|
|
4283
|
+
// packages/libs/manifest/src/resolve.ts
|
|
4306
4284
|
function resolveProjectGraph(manifest) {
|
|
4307
4285
|
return compileProjectGraph(manifest);
|
|
4308
4286
|
}
|
|
@@ -4403,9 +4381,16 @@ function executionService(service) {
|
|
|
4403
4381
|
}
|
|
4404
4382
|
var init_resolve = () => {};
|
|
4405
4383
|
|
|
4406
|
-
//
|
|
4384
|
+
// packages/libs/manifest/src/images.ts
|
|
4407
4385
|
import { readFile, realpath, stat } from "fs/promises";
|
|
4408
4386
|
import path from "path";
|
|
4387
|
+
function resolvePrebuiltImage(family, registry = process.env.SANDBLOCKS_PREBUILT_IMAGE_REGISTRY) {
|
|
4388
|
+
const image = SANDBLOCKS_PREBUILT_IMAGES[family];
|
|
4389
|
+
const configured = registry?.trim().replace(/\/$/, "");
|
|
4390
|
+
if (!configured)
|
|
4391
|
+
return image;
|
|
4392
|
+
return `${configured}/${image.slice(image.lastIndexOf("/") + 1)}`;
|
|
4393
|
+
}
|
|
4409
4394
|
async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".") {
|
|
4410
4395
|
const root = await realpath(repositoryRoot);
|
|
4411
4396
|
const workspace = await containedDirectory(root, workspaceRoot);
|
|
@@ -4436,7 +4421,7 @@ async function detectPrebuiltWorkspaceImage(repositoryRoot, workspaceRoot = ".")
|
|
|
4436
4421
|
return selection("node", "default Node.js environment");
|
|
4437
4422
|
}
|
|
4438
4423
|
function selection(family, evidence) {
|
|
4439
|
-
return { family, image:
|
|
4424
|
+
return { family, image: resolvePrebuiltImage(family), evidence };
|
|
4440
4425
|
}
|
|
4441
4426
|
async function containedDirectory(repositoryRoot, relative) {
|
|
4442
4427
|
const candidate = path.resolve(repositoryRoot, relative);
|
|
@@ -4502,14 +4487,14 @@ function relativeEvidence(repositoryRoot, directory, file) {
|
|
|
4502
4487
|
var SANDBLOCKS_PREBUILT_IMAGES, LOCKFILES;
|
|
4503
4488
|
var init_images = __esm(() => {
|
|
4504
4489
|
SANDBLOCKS_PREBUILT_IMAGES = {
|
|
4505
|
-
bun: "
|
|
4506
|
-
pnpm: "
|
|
4507
|
-
npm: "
|
|
4508
|
-
yarn: "
|
|
4509
|
-
corepack: "
|
|
4510
|
-
node: "
|
|
4511
|
-
"node-native": "
|
|
4512
|
-
playwright: "
|
|
4490
|
+
bun: "100.114.99.85:5000/sandblocks/executor-bun:1.0.0",
|
|
4491
|
+
pnpm: "100.114.99.85:5000/sandblocks/executor-pnpm:1.2.0",
|
|
4492
|
+
npm: "100.114.99.85:5000/sandblocks/executor-npm:1.0.0",
|
|
4493
|
+
yarn: "100.114.99.85:5000/sandblocks/executor-yarn:1.0.0",
|
|
4494
|
+
corepack: "100.114.99.85:5000/sandblocks/executor-corepack:1.0.0",
|
|
4495
|
+
node: "100.114.99.85:5000/sandblocks/executor-node:1.0.0",
|
|
4496
|
+
"node-native": "100.114.99.85:5000/sandblocks/executor-node-native:1.0.0",
|
|
4497
|
+
playwright: "100.114.99.85:5000/sandblocks/executor-playwright:1.0.0"
|
|
4513
4498
|
};
|
|
4514
4499
|
LOCKFILES = [
|
|
4515
4500
|
{ file: "bun.lock", family: "bun" },
|
|
@@ -4521,7 +4506,7 @@ var init_images = __esm(() => {
|
|
|
4521
4506
|
];
|
|
4522
4507
|
});
|
|
4523
4508
|
|
|
4524
|
-
//
|
|
4509
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
4525
4510
|
var require_identity = __commonJS((exports) => {
|
|
4526
4511
|
var ALIAS = Symbol.for("yaml.alias");
|
|
4527
4512
|
var DOC = Symbol.for("yaml.document");
|
|
@@ -4575,7 +4560,7 @@ var require_identity = __commonJS((exports) => {
|
|
|
4575
4560
|
exports.isSeq = isSeq;
|
|
4576
4561
|
});
|
|
4577
4562
|
|
|
4578
|
-
//
|
|
4563
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/visit.js
|
|
4579
4564
|
var require_visit = __commonJS((exports) => {
|
|
4580
4565
|
var identity = require_identity();
|
|
4581
4566
|
var BREAK = Symbol("break visit");
|
|
@@ -4730,7 +4715,7 @@ var require_visit = __commonJS((exports) => {
|
|
|
4730
4715
|
exports.visitAsync = visitAsync;
|
|
4731
4716
|
});
|
|
4732
4717
|
|
|
4733
|
-
//
|
|
4718
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js
|
|
4734
4719
|
var require_directives = __commonJS((exports) => {
|
|
4735
4720
|
var identity = require_identity();
|
|
4736
4721
|
var visit = require_visit();
|
|
@@ -4882,7 +4867,7 @@ var require_directives = __commonJS((exports) => {
|
|
|
4882
4867
|
exports.Directives = Directives;
|
|
4883
4868
|
});
|
|
4884
4869
|
|
|
4885
|
-
//
|
|
4870
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js
|
|
4886
4871
|
var require_anchors = __commonJS((exports) => {
|
|
4887
4872
|
var identity = require_identity();
|
|
4888
4873
|
var visit = require_visit();
|
|
@@ -4944,7 +4929,7 @@ var require_anchors = __commonJS((exports) => {
|
|
|
4944
4929
|
exports.findNewAnchor = findNewAnchor;
|
|
4945
4930
|
});
|
|
4946
4931
|
|
|
4947
|
-
//
|
|
4932
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/applyReviver.js
|
|
4948
4933
|
var require_applyReviver = __commonJS((exports) => {
|
|
4949
4934
|
function applyReviver(reviver, obj, key, val) {
|
|
4950
4935
|
if (val && typeof val === "object") {
|
|
@@ -4991,7 +4976,7 @@ var require_applyReviver = __commonJS((exports) => {
|
|
|
4991
4976
|
exports.applyReviver = applyReviver;
|
|
4992
4977
|
});
|
|
4993
4978
|
|
|
4994
|
-
//
|
|
4979
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/toJS.js
|
|
4995
4980
|
var require_toJS = __commonJS((exports) => {
|
|
4996
4981
|
var identity = require_identity();
|
|
4997
4982
|
function toJS(value, arg, ctx) {
|
|
@@ -5018,7 +5003,7 @@ var require_toJS = __commonJS((exports) => {
|
|
|
5018
5003
|
exports.toJS = toJS;
|
|
5019
5004
|
});
|
|
5020
5005
|
|
|
5021
|
-
//
|
|
5006
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Node.js
|
|
5022
5007
|
var require_Node = __commonJS((exports) => {
|
|
5023
5008
|
var applyReviver = require_applyReviver();
|
|
5024
5009
|
var identity = require_identity();
|
|
@@ -5055,7 +5040,7 @@ var require_Node = __commonJS((exports) => {
|
|
|
5055
5040
|
exports.NodeBase = NodeBase;
|
|
5056
5041
|
});
|
|
5057
5042
|
|
|
5058
|
-
//
|
|
5043
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js
|
|
5059
5044
|
var require_Alias = __commonJS((exports) => {
|
|
5060
5045
|
var anchors = require_anchors();
|
|
5061
5046
|
var visit = require_visit();
|
|
@@ -5165,7 +5150,7 @@ var require_Alias = __commonJS((exports) => {
|
|
|
5165
5150
|
exports.Alias = Alias;
|
|
5166
5151
|
});
|
|
5167
5152
|
|
|
5168
|
-
//
|
|
5153
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Scalar.js
|
|
5169
5154
|
var require_Scalar = __commonJS((exports) => {
|
|
5170
5155
|
var identity = require_identity();
|
|
5171
5156
|
var Node = require_Node();
|
|
@@ -5193,7 +5178,7 @@ var require_Scalar = __commonJS((exports) => {
|
|
|
5193
5178
|
exports.isScalarValue = isScalarValue;
|
|
5194
5179
|
});
|
|
5195
5180
|
|
|
5196
|
-
//
|
|
5181
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/createNode.js
|
|
5197
5182
|
var require_createNode = __commonJS((exports) => {
|
|
5198
5183
|
var Alias = require_Alias();
|
|
5199
5184
|
var identity = require_identity();
|
|
@@ -5265,7 +5250,7 @@ var require_createNode = __commonJS((exports) => {
|
|
|
5265
5250
|
exports.createNode = createNode;
|
|
5266
5251
|
});
|
|
5267
5252
|
|
|
5268
|
-
//
|
|
5253
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Collection.js
|
|
5269
5254
|
var require_Collection = __commonJS((exports) => {
|
|
5270
5255
|
var createNode = require_createNode();
|
|
5271
5256
|
var identity = require_identity();
|
|
@@ -5380,7 +5365,7 @@ var require_Collection = __commonJS((exports) => {
|
|
|
5380
5365
|
exports.isEmptyPath = isEmptyPath;
|
|
5381
5366
|
});
|
|
5382
5367
|
|
|
5383
|
-
//
|
|
5368
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyComment.js
|
|
5384
5369
|
var require_stringifyComment = __commonJS((exports) => {
|
|
5385
5370
|
var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#");
|
|
5386
5371
|
function indentComment(comment, indent) {
|
|
@@ -5397,7 +5382,7 @@ var require_stringifyComment = __commonJS((exports) => {
|
|
|
5397
5382
|
exports.stringifyComment = stringifyComment;
|
|
5398
5383
|
});
|
|
5399
5384
|
|
|
5400
|
-
//
|
|
5385
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/foldFlowLines.js
|
|
5401
5386
|
var require_foldFlowLines = __commonJS((exports) => {
|
|
5402
5387
|
var FOLD_FLOW = "flow";
|
|
5403
5388
|
var FOLD_BLOCK = "block";
|
|
@@ -5534,7 +5519,7 @@ ${indent}${text.slice(fold + 1, end2)}`;
|
|
|
5534
5519
|
exports.foldFlowLines = foldFlowLines;
|
|
5535
5520
|
});
|
|
5536
5521
|
|
|
5537
|
-
//
|
|
5522
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyString.js
|
|
5538
5523
|
var require_stringifyString = __commonJS((exports) => {
|
|
5539
5524
|
var Scalar = require_Scalar();
|
|
5540
5525
|
var foldFlowLines = require_foldFlowLines();
|
|
@@ -5832,7 +5817,7 @@ ${indent}`);
|
|
|
5832
5817
|
exports.stringifyString = stringifyString;
|
|
5833
5818
|
});
|
|
5834
5819
|
|
|
5835
|
-
//
|
|
5820
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringify.js
|
|
5836
5821
|
var require_stringify = __commonJS((exports) => {
|
|
5837
5822
|
var anchors = require_anchors();
|
|
5838
5823
|
var identity = require_identity();
|
|
@@ -5953,7 +5938,7 @@ ${ctx.indent}${str}`;
|
|
|
5953
5938
|
exports.stringify = stringify;
|
|
5954
5939
|
});
|
|
5955
5940
|
|
|
5956
|
-
//
|
|
5941
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyPair.js
|
|
5957
5942
|
var require_stringifyPair = __commonJS((exports) => {
|
|
5958
5943
|
var identity = require_identity();
|
|
5959
5944
|
var Scalar = require_Scalar();
|
|
@@ -6089,7 +6074,7 @@ ${ctx.indent}`;
|
|
|
6089
6074
|
exports.stringifyPair = stringifyPair;
|
|
6090
6075
|
});
|
|
6091
6076
|
|
|
6092
|
-
//
|
|
6077
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/log.js
|
|
6093
6078
|
var require_log = __commonJS((exports) => {
|
|
6094
6079
|
var node_process = __require("process");
|
|
6095
6080
|
function debug(logLevel, ...messages) {
|
|
@@ -6108,7 +6093,7 @@ var require_log = __commonJS((exports) => {
|
|
|
6108
6093
|
exports.warn = warn;
|
|
6109
6094
|
});
|
|
6110
6095
|
|
|
6111
|
-
//
|
|
6096
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/merge.js
|
|
6112
6097
|
var require_merge = __commonJS((exports) => {
|
|
6113
6098
|
var identity = require_identity();
|
|
6114
6099
|
var Scalar = require_Scalar();
|
|
@@ -6165,7 +6150,7 @@ var require_merge = __commonJS((exports) => {
|
|
|
6165
6150
|
exports.merge = merge;
|
|
6166
6151
|
});
|
|
6167
6152
|
|
|
6168
|
-
//
|
|
6153
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
|
|
6169
6154
|
var require_addPairToJSMap = __commonJS((exports) => {
|
|
6170
6155
|
var log = require_log();
|
|
6171
6156
|
var merge = require_merge();
|
|
@@ -6226,7 +6211,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
|
|
|
6226
6211
|
exports.addPairToJSMap = addPairToJSMap;
|
|
6227
6212
|
});
|
|
6228
6213
|
|
|
6229
|
-
//
|
|
6214
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/Pair.js
|
|
6230
6215
|
var require_Pair = __commonJS((exports) => {
|
|
6231
6216
|
var createNode = require_createNode();
|
|
6232
6217
|
var stringifyPair = require_stringifyPair();
|
|
@@ -6264,7 +6249,7 @@ var require_Pair = __commonJS((exports) => {
|
|
|
6264
6249
|
exports.createPair = createPair;
|
|
6265
6250
|
});
|
|
6266
6251
|
|
|
6267
|
-
//
|
|
6252
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyCollection.js
|
|
6268
6253
|
var require_stringifyCollection = __commonJS((exports) => {
|
|
6269
6254
|
var identity = require_identity();
|
|
6270
6255
|
var stringify = require_stringify();
|
|
@@ -6416,7 +6401,7 @@ ${indent}${end}`;
|
|
|
6416
6401
|
exports.stringifyCollection = stringifyCollection;
|
|
6417
6402
|
});
|
|
6418
6403
|
|
|
6419
|
-
//
|
|
6404
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLMap.js
|
|
6420
6405
|
var require_YAMLMap = __commonJS((exports) => {
|
|
6421
6406
|
var stringifyCollection = require_stringifyCollection();
|
|
6422
6407
|
var addPairToJSMap = require_addPairToJSMap();
|
|
@@ -6543,7 +6528,7 @@ var require_YAMLMap = __commonJS((exports) => {
|
|
|
6543
6528
|
exports.findPair = findPair;
|
|
6544
6529
|
});
|
|
6545
6530
|
|
|
6546
|
-
//
|
|
6531
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/map.js
|
|
6547
6532
|
var require_map = __commonJS((exports) => {
|
|
6548
6533
|
var identity = require_identity();
|
|
6549
6534
|
var YAMLMap = require_YAMLMap();
|
|
@@ -6562,7 +6547,7 @@ var require_map = __commonJS((exports) => {
|
|
|
6562
6547
|
exports.map = map;
|
|
6563
6548
|
});
|
|
6564
6549
|
|
|
6565
|
-
//
|
|
6550
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/YAMLSeq.js
|
|
6566
6551
|
var require_YAMLSeq = __commonJS((exports) => {
|
|
6567
6552
|
var createNode = require_createNode();
|
|
6568
6553
|
var stringifyCollection = require_stringifyCollection();
|
|
@@ -6655,7 +6640,7 @@ var require_YAMLSeq = __commonJS((exports) => {
|
|
|
6655
6640
|
exports.YAMLSeq = YAMLSeq;
|
|
6656
6641
|
});
|
|
6657
6642
|
|
|
6658
|
-
//
|
|
6643
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/seq.js
|
|
6659
6644
|
var require_seq = __commonJS((exports) => {
|
|
6660
6645
|
var identity = require_identity();
|
|
6661
6646
|
var YAMLSeq = require_YAMLSeq();
|
|
@@ -6674,7 +6659,7 @@ var require_seq = __commonJS((exports) => {
|
|
|
6674
6659
|
exports.seq = seq;
|
|
6675
6660
|
});
|
|
6676
6661
|
|
|
6677
|
-
//
|
|
6662
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/string.js
|
|
6678
6663
|
var require_string = __commonJS((exports) => {
|
|
6679
6664
|
var stringifyString = require_stringifyString();
|
|
6680
6665
|
var string = {
|
|
@@ -6690,7 +6675,7 @@ var require_string = __commonJS((exports) => {
|
|
|
6690
6675
|
exports.string = string;
|
|
6691
6676
|
});
|
|
6692
6677
|
|
|
6693
|
-
//
|
|
6678
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/common/null.js
|
|
6694
6679
|
var require_null = __commonJS((exports) => {
|
|
6695
6680
|
var Scalar = require_Scalar();
|
|
6696
6681
|
var nullTag = {
|
|
@@ -6705,7 +6690,7 @@ var require_null = __commonJS((exports) => {
|
|
|
6705
6690
|
exports.nullTag = nullTag;
|
|
6706
6691
|
});
|
|
6707
6692
|
|
|
6708
|
-
//
|
|
6693
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/bool.js
|
|
6709
6694
|
var require_bool = __commonJS((exports) => {
|
|
6710
6695
|
var Scalar = require_Scalar();
|
|
6711
6696
|
var boolTag = {
|
|
@@ -6726,7 +6711,7 @@ var require_bool = __commonJS((exports) => {
|
|
|
6726
6711
|
exports.boolTag = boolTag;
|
|
6727
6712
|
});
|
|
6728
6713
|
|
|
6729
|
-
//
|
|
6714
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyNumber.js
|
|
6730
6715
|
var require_stringifyNumber = __commonJS((exports) => {
|
|
6731
6716
|
function stringifyNumber({ format, minFractionDigits, tag, value }) {
|
|
6732
6717
|
if (typeof value === "bigint")
|
|
@@ -6750,7 +6735,7 @@ var require_stringifyNumber = __commonJS((exports) => {
|
|
|
6750
6735
|
exports.stringifyNumber = stringifyNumber;
|
|
6751
6736
|
});
|
|
6752
6737
|
|
|
6753
|
-
//
|
|
6738
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/float.js
|
|
6754
6739
|
var require_float = __commonJS((exports) => {
|
|
6755
6740
|
var Scalar = require_Scalar();
|
|
6756
6741
|
var stringifyNumber = require_stringifyNumber();
|
|
@@ -6793,7 +6778,7 @@ var require_float = __commonJS((exports) => {
|
|
|
6793
6778
|
exports.floatNaN = floatNaN;
|
|
6794
6779
|
});
|
|
6795
6780
|
|
|
6796
|
-
//
|
|
6781
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/int.js
|
|
6797
6782
|
var require_int = __commonJS((exports) => {
|
|
6798
6783
|
var stringifyNumber = require_stringifyNumber();
|
|
6799
6784
|
var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
|
|
@@ -6835,7 +6820,7 @@ var require_int = __commonJS((exports) => {
|
|
|
6835
6820
|
exports.intOct = intOct;
|
|
6836
6821
|
});
|
|
6837
6822
|
|
|
6838
|
-
//
|
|
6823
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/core/schema.js
|
|
6839
6824
|
var require_schema = __commonJS((exports) => {
|
|
6840
6825
|
var map = require_map();
|
|
6841
6826
|
var _null = require_null();
|
|
@@ -6860,7 +6845,7 @@ var require_schema = __commonJS((exports) => {
|
|
|
6860
6845
|
exports.schema = schema;
|
|
6861
6846
|
});
|
|
6862
6847
|
|
|
6863
|
-
//
|
|
6848
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/json/schema.js
|
|
6864
6849
|
var require_schema2 = __commonJS((exports) => {
|
|
6865
6850
|
var Scalar = require_Scalar();
|
|
6866
6851
|
var map = require_map();
|
|
@@ -6924,7 +6909,7 @@ var require_schema2 = __commonJS((exports) => {
|
|
|
6924
6909
|
exports.schema = schema;
|
|
6925
6910
|
});
|
|
6926
6911
|
|
|
6927
|
-
//
|
|
6912
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/binary.js
|
|
6928
6913
|
var require_binary = __commonJS((exports) => {
|
|
6929
6914
|
var node_buffer = __require("buffer");
|
|
6930
6915
|
var Scalar = require_Scalar();
|
|
@@ -6979,7 +6964,7 @@ var require_binary = __commonJS((exports) => {
|
|
|
6979
6964
|
exports.binary = binary;
|
|
6980
6965
|
});
|
|
6981
6966
|
|
|
6982
|
-
//
|
|
6967
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/pairs.js
|
|
6983
6968
|
var require_pairs = __commonJS((exports) => {
|
|
6984
6969
|
var identity = require_identity();
|
|
6985
6970
|
var Pair = require_Pair();
|
|
@@ -7054,7 +7039,7 @@ ${cn.comment}` : item.comment;
|
|
|
7054
7039
|
exports.resolvePairs = resolvePairs;
|
|
7055
7040
|
});
|
|
7056
7041
|
|
|
7057
|
-
//
|
|
7042
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/omap.js
|
|
7058
7043
|
var require_omap = __commonJS((exports) => {
|
|
7059
7044
|
var identity = require_identity();
|
|
7060
7045
|
var toJS = require_toJS();
|
|
@@ -7126,7 +7111,7 @@ var require_omap = __commonJS((exports) => {
|
|
|
7126
7111
|
exports.omap = omap;
|
|
7127
7112
|
});
|
|
7128
7113
|
|
|
7129
|
-
//
|
|
7114
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/bool.js
|
|
7130
7115
|
var require_bool2 = __commonJS((exports) => {
|
|
7131
7116
|
var Scalar = require_Scalar();
|
|
7132
7117
|
function boolStringify({ value, source }, ctx) {
|
|
@@ -7155,7 +7140,7 @@ var require_bool2 = __commonJS((exports) => {
|
|
|
7155
7140
|
exports.trueTag = trueTag;
|
|
7156
7141
|
});
|
|
7157
7142
|
|
|
7158
|
-
//
|
|
7143
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/float.js
|
|
7159
7144
|
var require_float2 = __commonJS((exports) => {
|
|
7160
7145
|
var Scalar = require_Scalar();
|
|
7161
7146
|
var stringifyNumber = require_stringifyNumber();
|
|
@@ -7201,7 +7186,7 @@ var require_float2 = __commonJS((exports) => {
|
|
|
7201
7186
|
exports.floatNaN = floatNaN;
|
|
7202
7187
|
});
|
|
7203
7188
|
|
|
7204
|
-
//
|
|
7189
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/int.js
|
|
7205
7190
|
var require_int2 = __commonJS((exports) => {
|
|
7206
7191
|
var stringifyNumber = require_stringifyNumber();
|
|
7207
7192
|
var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
|
|
@@ -7277,7 +7262,7 @@ var require_int2 = __commonJS((exports) => {
|
|
|
7277
7262
|
exports.intOct = intOct;
|
|
7278
7263
|
});
|
|
7279
7264
|
|
|
7280
|
-
//
|
|
7265
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/set.js
|
|
7281
7266
|
var require_set = __commonJS((exports) => {
|
|
7282
7267
|
var identity = require_identity();
|
|
7283
7268
|
var Pair = require_Pair();
|
|
@@ -7360,7 +7345,7 @@ var require_set = __commonJS((exports) => {
|
|
|
7360
7345
|
exports.set = set;
|
|
7361
7346
|
});
|
|
7362
7347
|
|
|
7363
|
-
//
|
|
7348
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js
|
|
7364
7349
|
var require_timestamp = __commonJS((exports) => {
|
|
7365
7350
|
var stringifyNumber = require_stringifyNumber();
|
|
7366
7351
|
function parseSexagesimal(str, asBigInt) {
|
|
@@ -7442,7 +7427,7 @@ var require_timestamp = __commonJS((exports) => {
|
|
|
7442
7427
|
exports.timestamp = timestamp;
|
|
7443
7428
|
});
|
|
7444
7429
|
|
|
7445
|
-
//
|
|
7430
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
|
|
7446
7431
|
var require_schema3 = __commonJS((exports) => {
|
|
7447
7432
|
var map = require_map();
|
|
7448
7433
|
var _null = require_null();
|
|
@@ -7483,7 +7468,7 @@ var require_schema3 = __commonJS((exports) => {
|
|
|
7483
7468
|
exports.schema = schema;
|
|
7484
7469
|
});
|
|
7485
7470
|
|
|
7486
|
-
//
|
|
7471
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/tags.js
|
|
7487
7472
|
var require_tags = __commonJS((exports) => {
|
|
7488
7473
|
var map = require_map();
|
|
7489
7474
|
var _null = require_null();
|
|
@@ -7574,7 +7559,7 @@ var require_tags = __commonJS((exports) => {
|
|
|
7574
7559
|
exports.getTags = getTags;
|
|
7575
7560
|
});
|
|
7576
7561
|
|
|
7577
|
-
//
|
|
7562
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/Schema.js
|
|
7578
7563
|
var require_Schema = __commonJS((exports) => {
|
|
7579
7564
|
var identity = require_identity();
|
|
7580
7565
|
var map = require_map();
|
|
@@ -7604,7 +7589,7 @@ var require_Schema = __commonJS((exports) => {
|
|
|
7604
7589
|
exports.Schema = Schema;
|
|
7605
7590
|
});
|
|
7606
7591
|
|
|
7607
|
-
//
|
|
7592
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyDocument.js
|
|
7608
7593
|
var require_stringifyDocument = __commonJS((exports) => {
|
|
7609
7594
|
var identity = require_identity();
|
|
7610
7595
|
var stringify = require_stringify();
|
|
@@ -7684,7 +7669,7 @@ var require_stringifyDocument = __commonJS((exports) => {
|
|
|
7684
7669
|
exports.stringifyDocument = stringifyDocument;
|
|
7685
7670
|
});
|
|
7686
7671
|
|
|
7687
|
-
//
|
|
7672
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/doc/Document.js
|
|
7688
7673
|
var require_Document = __commonJS((exports) => {
|
|
7689
7674
|
var Alias = require_Alias();
|
|
7690
7675
|
var Collection = require_Collection();
|
|
@@ -7919,7 +7904,7 @@ var require_Document = __commonJS((exports) => {
|
|
|
7919
7904
|
exports.Document = Document;
|
|
7920
7905
|
});
|
|
7921
7906
|
|
|
7922
|
-
//
|
|
7907
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/errors.js
|
|
7923
7908
|
var require_errors = __commonJS((exports) => {
|
|
7924
7909
|
class YAMLError extends Error {
|
|
7925
7910
|
constructor(name, pos, code, message) {
|
|
@@ -7984,7 +7969,7 @@ ${pointer}
|
|
|
7984
7969
|
exports.prettifyError = prettifyError;
|
|
7985
7970
|
});
|
|
7986
7971
|
|
|
7987
|
-
//
|
|
7972
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-props.js
|
|
7988
7973
|
var require_resolve_props = __commonJS((exports) => {
|
|
7989
7974
|
function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) {
|
|
7990
7975
|
let spaceBefore = false;
|
|
@@ -8114,7 +8099,7 @@ var require_resolve_props = __commonJS((exports) => {
|
|
|
8114
8099
|
exports.resolveProps = resolveProps;
|
|
8115
8100
|
});
|
|
8116
8101
|
|
|
8117
|
-
//
|
|
8102
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-contains-newline.js
|
|
8118
8103
|
var require_util_contains_newline = __commonJS((exports) => {
|
|
8119
8104
|
function containsNewline(key) {
|
|
8120
8105
|
if (!key)
|
|
@@ -8154,7 +8139,7 @@ var require_util_contains_newline = __commonJS((exports) => {
|
|
|
8154
8139
|
exports.containsNewline = containsNewline;
|
|
8155
8140
|
});
|
|
8156
8141
|
|
|
8157
|
-
//
|
|
8142
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-flow-indent-check.js
|
|
8158
8143
|
var require_util_flow_indent_check = __commonJS((exports) => {
|
|
8159
8144
|
var utilContainsNewline = require_util_contains_newline();
|
|
8160
8145
|
function flowIndentCheck(indent, fc, onError) {
|
|
@@ -8169,7 +8154,7 @@ var require_util_flow_indent_check = __commonJS((exports) => {
|
|
|
8169
8154
|
exports.flowIndentCheck = flowIndentCheck;
|
|
8170
8155
|
});
|
|
8171
8156
|
|
|
8172
|
-
//
|
|
8157
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-map-includes.js
|
|
8173
8158
|
var require_util_map_includes = __commonJS((exports) => {
|
|
8174
8159
|
var identity = require_identity();
|
|
8175
8160
|
function mapIncludes(ctx, items, search) {
|
|
@@ -8182,7 +8167,7 @@ var require_util_map_includes = __commonJS((exports) => {
|
|
|
8182
8167
|
exports.mapIncludes = mapIncludes;
|
|
8183
8168
|
});
|
|
8184
8169
|
|
|
8185
|
-
//
|
|
8170
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-map.js
|
|
8186
8171
|
var require_resolve_block_map = __commonJS((exports) => {
|
|
8187
8172
|
var Pair = require_Pair();
|
|
8188
8173
|
var YAMLMap = require_YAMLMap();
|
|
@@ -8289,7 +8274,7 @@ var require_resolve_block_map = __commonJS((exports) => {
|
|
|
8289
8274
|
exports.resolveBlockMap = resolveBlockMap;
|
|
8290
8275
|
});
|
|
8291
8276
|
|
|
8292
|
-
//
|
|
8277
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-seq.js
|
|
8293
8278
|
var require_resolve_block_seq = __commonJS((exports) => {
|
|
8294
8279
|
var YAMLSeq = require_YAMLSeq();
|
|
8295
8280
|
var resolveProps = require_resolve_props();
|
|
@@ -8337,7 +8322,7 @@ var require_resolve_block_seq = __commonJS((exports) => {
|
|
|
8337
8322
|
exports.resolveBlockSeq = resolveBlockSeq;
|
|
8338
8323
|
});
|
|
8339
8324
|
|
|
8340
|
-
//
|
|
8325
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-end.js
|
|
8341
8326
|
var require_resolve_end = __commonJS((exports) => {
|
|
8342
8327
|
function resolveEnd(end, offset, reqSpace, onError) {
|
|
8343
8328
|
let comment = "";
|
|
@@ -8377,7 +8362,7 @@ var require_resolve_end = __commonJS((exports) => {
|
|
|
8377
8362
|
exports.resolveEnd = resolveEnd;
|
|
8378
8363
|
});
|
|
8379
8364
|
|
|
8380
|
-
//
|
|
8365
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-collection.js
|
|
8381
8366
|
var require_resolve_flow_collection = __commonJS((exports) => {
|
|
8382
8367
|
var identity = require_identity();
|
|
8383
8368
|
var Pair = require_Pair();
|
|
@@ -8568,7 +8553,7 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
8568
8553
|
exports.resolveFlowCollection = resolveFlowCollection;
|
|
8569
8554
|
});
|
|
8570
8555
|
|
|
8571
|
-
//
|
|
8556
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-collection.js
|
|
8572
8557
|
var require_compose_collection = __commonJS((exports) => {
|
|
8573
8558
|
var identity = require_identity();
|
|
8574
8559
|
var Scalar = require_Scalar();
|
|
@@ -8630,7 +8615,7 @@ var require_compose_collection = __commonJS((exports) => {
|
|
|
8630
8615
|
exports.composeCollection = composeCollection;
|
|
8631
8616
|
});
|
|
8632
8617
|
|
|
8633
|
-
//
|
|
8618
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-block-scalar.js
|
|
8634
8619
|
var require_resolve_block_scalar = __commonJS((exports) => {
|
|
8635
8620
|
var Scalar = require_Scalar();
|
|
8636
8621
|
function resolveBlockScalar(ctx, scalar, onError) {
|
|
@@ -8823,7 +8808,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
|
|
|
8823
8808
|
exports.resolveBlockScalar = resolveBlockScalar;
|
|
8824
8809
|
});
|
|
8825
8810
|
|
|
8826
|
-
//
|
|
8811
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/resolve-flow-scalar.js
|
|
8827
8812
|
var require_resolve_flow_scalar = __commonJS((exports) => {
|
|
8828
8813
|
var Scalar = require_Scalar();
|
|
8829
8814
|
var resolveEnd = require_resolve_end();
|
|
@@ -9040,7 +9025,7 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
|
|
|
9040
9025
|
exports.resolveFlowScalar = resolveFlowScalar;
|
|
9041
9026
|
});
|
|
9042
9027
|
|
|
9043
|
-
//
|
|
9028
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-scalar.js
|
|
9044
9029
|
var require_compose_scalar = __commonJS((exports) => {
|
|
9045
9030
|
var identity = require_identity();
|
|
9046
9031
|
var Scalar = require_Scalar();
|
|
@@ -9118,7 +9103,7 @@ var require_compose_scalar = __commonJS((exports) => {
|
|
|
9118
9103
|
exports.composeScalar = composeScalar;
|
|
9119
9104
|
});
|
|
9120
9105
|
|
|
9121
|
-
//
|
|
9106
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/util-empty-scalar-position.js
|
|
9122
9107
|
var require_util_empty_scalar_position = __commonJS((exports) => {
|
|
9123
9108
|
function emptyScalarPosition(offset, before, pos) {
|
|
9124
9109
|
if (before) {
|
|
@@ -9145,7 +9130,7 @@ var require_util_empty_scalar_position = __commonJS((exports) => {
|
|
|
9145
9130
|
exports.emptyScalarPosition = emptyScalarPosition;
|
|
9146
9131
|
});
|
|
9147
9132
|
|
|
9148
|
-
//
|
|
9133
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-node.js
|
|
9149
9134
|
var require_compose_node = __commonJS((exports) => {
|
|
9150
9135
|
var Alias = require_Alias();
|
|
9151
9136
|
var identity = require_identity();
|
|
@@ -9248,7 +9233,7 @@ var require_compose_node = __commonJS((exports) => {
|
|
|
9248
9233
|
exports.composeNode = composeNode;
|
|
9249
9234
|
});
|
|
9250
9235
|
|
|
9251
|
-
//
|
|
9236
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/compose-doc.js
|
|
9252
9237
|
var require_compose_doc = __commonJS((exports) => {
|
|
9253
9238
|
var Document = require_Document();
|
|
9254
9239
|
var composeNode = require_compose_node();
|
|
@@ -9288,7 +9273,7 @@ var require_compose_doc = __commonJS((exports) => {
|
|
|
9288
9273
|
exports.composeDoc = composeDoc;
|
|
9289
9274
|
});
|
|
9290
9275
|
|
|
9291
|
-
//
|
|
9276
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/compose/composer.js
|
|
9292
9277
|
var require_composer = __commonJS((exports) => {
|
|
9293
9278
|
var node_process = __require("process");
|
|
9294
9279
|
var directives = require_directives();
|
|
@@ -9479,7 +9464,7 @@ ${end.comment}` : end.comment;
|
|
|
9479
9464
|
exports.Composer = Composer;
|
|
9480
9465
|
});
|
|
9481
9466
|
|
|
9482
|
-
//
|
|
9467
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-scalar.js
|
|
9483
9468
|
var require_cst_scalar = __commonJS((exports) => {
|
|
9484
9469
|
var resolveBlockScalar = require_resolve_block_scalar();
|
|
9485
9470
|
var resolveFlowScalar = require_resolve_flow_scalar();
|
|
@@ -9669,7 +9654,7 @@ var require_cst_scalar = __commonJS((exports) => {
|
|
|
9669
9654
|
exports.setScalarValue = setScalarValue;
|
|
9670
9655
|
});
|
|
9671
9656
|
|
|
9672
|
-
//
|
|
9657
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-stringify.js
|
|
9673
9658
|
var require_cst_stringify = __commonJS((exports) => {
|
|
9674
9659
|
var stringify = (cst) => ("type" in cst) ? stringifyToken(cst) : stringifyItem(cst);
|
|
9675
9660
|
function stringifyToken(token) {
|
|
@@ -9727,7 +9712,7 @@ var require_cst_stringify = __commonJS((exports) => {
|
|
|
9727
9712
|
exports.stringify = stringify;
|
|
9728
9713
|
});
|
|
9729
9714
|
|
|
9730
|
-
//
|
|
9715
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst-visit.js
|
|
9731
9716
|
var require_cst_visit = __commonJS((exports) => {
|
|
9732
9717
|
var BREAK = Symbol("break visit");
|
|
9733
9718
|
var SKIP = Symbol("skip children");
|
|
@@ -9786,7 +9771,7 @@ var require_cst_visit = __commonJS((exports) => {
|
|
|
9786
9771
|
exports.visit = visit;
|
|
9787
9772
|
});
|
|
9788
9773
|
|
|
9789
|
-
//
|
|
9774
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/cst.js
|
|
9790
9775
|
var require_cst = __commonJS((exports) => {
|
|
9791
9776
|
var cstScalar = require_cst_scalar();
|
|
9792
9777
|
var cstStringify = require_cst_stringify();
|
|
@@ -9887,7 +9872,7 @@ var require_cst = __commonJS((exports) => {
|
|
|
9887
9872
|
exports.tokenType = tokenType;
|
|
9888
9873
|
});
|
|
9889
9874
|
|
|
9890
|
-
//
|
|
9875
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/lexer.js
|
|
9891
9876
|
var require_lexer = __commonJS((exports) => {
|
|
9892
9877
|
var cst = require_cst();
|
|
9893
9878
|
function isEmpty(ch) {
|
|
@@ -10484,7 +10469,7 @@ var require_lexer = __commonJS((exports) => {
|
|
|
10484
10469
|
exports.Lexer = Lexer;
|
|
10485
10470
|
});
|
|
10486
10471
|
|
|
10487
|
-
//
|
|
10472
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/line-counter.js
|
|
10488
10473
|
var require_line_counter = __commonJS((exports) => {
|
|
10489
10474
|
class LineCounter {
|
|
10490
10475
|
constructor() {
|
|
@@ -10512,7 +10497,7 @@ var require_line_counter = __commonJS((exports) => {
|
|
|
10512
10497
|
exports.LineCounter = LineCounter;
|
|
10513
10498
|
});
|
|
10514
10499
|
|
|
10515
|
-
//
|
|
10500
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/parse/parser.js
|
|
10516
10501
|
var require_parser = __commonJS((exports) => {
|
|
10517
10502
|
var node_process = __require("process");
|
|
10518
10503
|
var cst = require_cst();
|
|
@@ -11368,7 +11353,7 @@ var require_parser = __commonJS((exports) => {
|
|
|
11368
11353
|
exports.Parser = Parser;
|
|
11369
11354
|
});
|
|
11370
11355
|
|
|
11371
|
-
//
|
|
11356
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/public-api.js
|
|
11372
11357
|
var require_public_api = __commonJS((exports) => {
|
|
11373
11358
|
var composer = require_composer();
|
|
11374
11359
|
var Document = require_Document();
|
|
@@ -11462,7 +11447,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
11462
11447
|
exports.stringify = stringify;
|
|
11463
11448
|
});
|
|
11464
11449
|
|
|
11465
|
-
//
|
|
11450
|
+
// node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
|
|
11466
11451
|
var composer, Document, Schema, errors2, Alias, identity, Pair, Scalar, YAMLMap, YAMLSeq, cst, lexer, lineCounter, parser, publicApi, visit, $Composer, $Document, $Schema, $YAMLError, $YAMLParseError, $YAMLWarning, $Alias, $isAlias, $isCollection, $isDocument, $isMap, $isNode, $isPair, $isScalar, $isSeq, $Pair, $Scalar, $YAMLMap, $YAMLSeq, $Lexer, $LineCounter, $Parser, $parse, $parseAllDocuments, $parseDocument, $stringify, $visit, $visitAsync;
|
|
11467
11452
|
var init_dist = __esm(() => {
|
|
11468
11453
|
composer = require_composer();
|
|
@@ -11511,7 +11496,7 @@ var init_dist = __esm(() => {
|
|
|
11511
11496
|
$visitAsync = visit.visitAsync;
|
|
11512
11497
|
});
|
|
11513
11498
|
|
|
11514
|
-
//
|
|
11499
|
+
// packages/libs/manifest/src/loader.ts
|
|
11515
11500
|
import { createHash } from "crypto";
|
|
11516
11501
|
import { readFile as readFile2, realpath as realpath2 } from "fs/promises";
|
|
11517
11502
|
import path2 from "path";
|
|
@@ -11698,12 +11683,13 @@ var init_loader = __esm(() => {
|
|
|
11698
11683
|
};
|
|
11699
11684
|
});
|
|
11700
11685
|
|
|
11701
|
-
//
|
|
11686
|
+
// packages/libs/manifest/src/index.ts
|
|
11702
11687
|
var exports_src = {};
|
|
11703
11688
|
__export(exports_src, {
|
|
11704
11689
|
sandblocksManifestDigest: () => sandblocksManifestDigest,
|
|
11705
11690
|
resolveServiceSteps: () => resolveServiceSteps,
|
|
11706
11691
|
resolveProjectGraph: () => resolveProjectGraph,
|
|
11692
|
+
resolvePrebuiltImage: () => resolvePrebuiltImage,
|
|
11707
11693
|
resolveEnvironment: () => resolveEnvironment,
|
|
11708
11694
|
parseSandblocksManifest: () => parseSandblocksManifest,
|
|
11709
11695
|
normalizeCommand: () => normalizeCommand,
|
|
@@ -11731,1715 +11717,2153 @@ var init_src = __esm(() => {
|
|
|
11731
11717
|
init_loader();
|
|
11732
11718
|
});
|
|
11733
11719
|
|
|
11734
|
-
// src/cli.ts
|
|
11720
|
+
// packages/libs/cli/src/cli.ts
|
|
11735
11721
|
init_src();
|
|
11736
11722
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
11737
|
-
import { lstat, readFile as readFile8, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
11723
|
+
import { lstat, readFile as readFile8, readdir as readdir2, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
11738
11724
|
import path6 from "path";
|
|
11739
11725
|
|
|
11740
|
-
//
|
|
11741
|
-
import {
|
|
11742
|
-
import
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11726
|
+
// packages/libs/sdk/dist/index.js
|
|
11727
|
+
import { randomUUID } from "crypto";
|
|
11728
|
+
import { appendFile, cp, mkdir, readFile as readFile3, rm } from "fs/promises";
|
|
11729
|
+
import { dirname, resolve } from "path";
|
|
11730
|
+
import { spawn } from "child_process";
|
|
11731
|
+
import { access, mkdir as mkdir2, writeFile } from "fs/promises";
|
|
11732
|
+
import { resolve as resolve2 } from "path";
|
|
11733
|
+
|
|
11734
|
+
class CommandAgent {
|
|
11735
|
+
options;
|
|
11736
|
+
id;
|
|
11737
|
+
constructor(options) {
|
|
11738
|
+
this.options = options;
|
|
11739
|
+
this.id = options.id ?? "command";
|
|
11752
11740
|
}
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11741
|
+
async run(context) {
|
|
11742
|
+
const startedAt = new Date().toISOString();
|
|
11743
|
+
const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
|
|
11744
|
+
const promptMode = this.options.prompt ?? "stdin";
|
|
11745
|
+
const command = promptMode === "argument" ? [...base, context.prompt] : base;
|
|
11746
|
+
const idle = new AbortController;
|
|
11747
|
+
const completed = new AbortController;
|
|
11748
|
+
let idleTimer;
|
|
11749
|
+
let completionTimer;
|
|
11750
|
+
const resetIdle = () => {
|
|
11751
|
+
if (idleTimer)
|
|
11752
|
+
clearTimeout(idleTimer);
|
|
11753
|
+
idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
|
|
11754
|
+
};
|
|
11755
|
+
resetIdle();
|
|
11756
|
+
let output = "";
|
|
11757
|
+
let stdout = "";
|
|
11758
|
+
let stderr = "";
|
|
11759
|
+
let lineBuffer = "";
|
|
11760
|
+
let completionSignal;
|
|
11761
|
+
const consume = async (chunk, stream) => {
|
|
11762
|
+
resetIdle();
|
|
11763
|
+
output += chunk;
|
|
11764
|
+
if (stream === "stdout")
|
|
11765
|
+
stdout += chunk;
|
|
11766
|
+
else
|
|
11767
|
+
stderr += chunk;
|
|
11768
|
+
lineBuffer += chunk;
|
|
11769
|
+
const lines = lineBuffer.split(/\r?\n/);
|
|
11770
|
+
lineBuffer = lines.pop() ?? "";
|
|
11771
|
+
for (const line of lines)
|
|
11772
|
+
await this.emitLine(line, stream, context);
|
|
11773
|
+
completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
|
|
11774
|
+
if (completionSignal && !completionTimer) {
|
|
11775
|
+
completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
|
|
11776
|
+
}
|
|
11777
|
+
};
|
|
11778
|
+
try {
|
|
11779
|
+
const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
|
|
11780
|
+
try {
|
|
11781
|
+
const result = await context.runtime.exec(command, {
|
|
11782
|
+
env: this.options.env,
|
|
11783
|
+
stdin: promptMode === "stdin" ? context.prompt : undefined,
|
|
11784
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
|
|
11785
|
+
onStdout: (chunk) => consume(chunk, "stdout"),
|
|
11786
|
+
onStderr: (chunk) => consume(chunk, "stderr")
|
|
11787
|
+
});
|
|
11788
|
+
stdout = result.stdout;
|
|
11789
|
+
stderr = result.stderr;
|
|
11790
|
+
return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
|
|
11791
|
+
} catch (error) {
|
|
11792
|
+
if (!completionSignal)
|
|
11793
|
+
throw error;
|
|
11794
|
+
return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
|
|
11795
|
+
}
|
|
11796
|
+
} finally {
|
|
11797
|
+
if (idleTimer)
|
|
11798
|
+
clearTimeout(idleTimer);
|
|
11799
|
+
if (completionTimer)
|
|
11800
|
+
clearTimeout(completionTimer);
|
|
11801
|
+
if (lineBuffer)
|
|
11802
|
+
await this.emitLine(lineBuffer, "stdout", context);
|
|
11803
|
+
}
|
|
11771
11804
|
}
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
if (await exists(backupPath))
|
|
11785
|
-
console.log(`chaining ${backupPath}`);
|
|
11786
|
-
}
|
|
11787
|
-
async function uninstall(root) {
|
|
11788
|
-
const hooksDir = await gitPath(root, "hooks");
|
|
11789
|
-
const hookPath = path3.join(hooksDir, "post-commit");
|
|
11790
|
-
const backupPath = path3.join(hooksDir, backupName);
|
|
11791
|
-
const current = await text(hookPath);
|
|
11792
|
-
if (!current?.includes(marker))
|
|
11793
|
-
throw new Error("Sandblocks post-commit hook is not installed");
|
|
11794
|
-
if (await exists(backupPath)) {
|
|
11795
|
-
await copyFile(backupPath, hookPath);
|
|
11796
|
-
await chmod(hookPath, 493);
|
|
11797
|
-
await rm(backupPath);
|
|
11798
|
-
console.log(`restored ${hookPath}`);
|
|
11799
|
-
} else {
|
|
11800
|
-
await rm(hookPath);
|
|
11801
|
-
console.log(`removed ${hookPath}`);
|
|
11805
|
+
result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
|
|
11806
|
+
const sessionId = this.options.sessionId?.(stdout, stderr);
|
|
11807
|
+
return {
|
|
11808
|
+
iteration: context.iteration,
|
|
11809
|
+
exitCode,
|
|
11810
|
+
stdout,
|
|
11811
|
+
stderr,
|
|
11812
|
+
...sessionId ? { sessionId } : {},
|
|
11813
|
+
...completionSignal ? { completionSignal } : {},
|
|
11814
|
+
startedAt,
|
|
11815
|
+
finishedAt: new Date().toISOString()
|
|
11816
|
+
};
|
|
11802
11817
|
}
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11818
|
+
async emitLine(line, stream, context) {
|
|
11819
|
+
const timestamp = new Date().toISOString();
|
|
11820
|
+
const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
|
|
11821
|
+
const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
|
|
11822
|
+
{
|
|
11823
|
+
type: "raw",
|
|
11824
|
+
line: stream === "stderr" ? `[stderr] ${line}` : line,
|
|
11825
|
+
iteration: context.iteration,
|
|
11826
|
+
timestamp
|
|
11827
|
+
}
|
|
11828
|
+
];
|
|
11829
|
+
for (const event of events)
|
|
11830
|
+
await context.emit(event);
|
|
11814
11831
|
}
|
|
11815
11832
|
}
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11833
|
+
|
|
11834
|
+
class ClaudeCodeAgent extends CommandAgent {
|
|
11835
|
+
constructor(options) {
|
|
11836
|
+
super({
|
|
11837
|
+
id: "claude-code",
|
|
11838
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
11839
|
+
prompt: "argument",
|
|
11840
|
+
command: (context) => [
|
|
11841
|
+
options.binary ?? "claude",
|
|
11842
|
+
"--print",
|
|
11843
|
+
"--model",
|
|
11844
|
+
options.model,
|
|
11845
|
+
...options.effort ? ["--effort", options.effort] : [],
|
|
11846
|
+
...options.extraArgs ?? []
|
|
11847
|
+
],
|
|
11848
|
+
sessionId: extractSessionId,
|
|
11849
|
+
parseEvent: parseJsonAgentEvent
|
|
11850
|
+
});
|
|
11828
11851
|
}
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11852
|
+
}
|
|
11853
|
+
|
|
11854
|
+
class CodexAgent extends CommandAgent {
|
|
11855
|
+
constructor(options) {
|
|
11856
|
+
super({
|
|
11857
|
+
id: "codex",
|
|
11858
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
11859
|
+
prompt: "argument",
|
|
11860
|
+
command: (context) => [
|
|
11861
|
+
options.binary ?? "codex",
|
|
11862
|
+
"exec",
|
|
11863
|
+
"--model",
|
|
11864
|
+
options.model,
|
|
11865
|
+
...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
|
|
11866
|
+
...options.extraArgs ?? []
|
|
11867
|
+
],
|
|
11868
|
+
sessionId: extractSessionId,
|
|
11869
|
+
parseEvent: parseJsonAgentEvent
|
|
11844
11870
|
});
|
|
11845
|
-
proc.unref();
|
|
11846
|
-
console.log(`sandblocks ${command} started for ${policy.environment} (${log})`);
|
|
11847
|
-
return;
|
|
11848
11871
|
}
|
|
11849
|
-
await dependencies.runSandbox(command, commandArgs);
|
|
11850
11872
|
}
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11873
|
+
|
|
11874
|
+
class PiAgent extends CommandAgent {
|
|
11875
|
+
constructor(options) {
|
|
11876
|
+
super({
|
|
11877
|
+
id: "pi",
|
|
11878
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
11879
|
+
prompt: "argument",
|
|
11880
|
+
command: (context) => [
|
|
11881
|
+
options.binary ?? "pi",
|
|
11882
|
+
"--print",
|
|
11883
|
+
"--model",
|
|
11884
|
+
options.model,
|
|
11885
|
+
...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
|
|
11886
|
+
...options.extraArgs ?? []
|
|
11887
|
+
],
|
|
11888
|
+
sessionId: extractSessionId,
|
|
11889
|
+
parseEvent: parseJsonAgentEvent
|
|
11890
|
+
});
|
|
11891
|
+
}
|
|
11854
11892
|
}
|
|
11855
|
-
|
|
11856
|
-
|
|
11893
|
+
function parseJsonAgentEvent(line, context) {
|
|
11894
|
+
let value;
|
|
11895
|
+
try {
|
|
11896
|
+
value = JSON.parse(line);
|
|
11897
|
+
} catch {
|
|
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
|
+
};
|
|
11908
|
+
}
|
|
11909
|
+
const text = value.text ?? value.message ?? value.content;
|
|
11910
|
+
return typeof text === "string" ? { type: "text", text, ...context } : { type: "raw", line, ...context };
|
|
11857
11911
|
}
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
new
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
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 };
|
|
11868
11924
|
}
|
|
11869
|
-
|
|
11870
|
-
|
|
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];
|
|
11871
11929
|
}
|
|
11872
|
-
|
|
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 };
|
|
11936
|
+
}
|
|
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;
|
|
11873
11949
|
try {
|
|
11874
|
-
|
|
11950
|
+
value = JSON.parse(content);
|
|
11875
11951
|
} catch {
|
|
11876
|
-
|
|
11952
|
+
throw new Error(`structured output <${spec.tag}> is not valid JSON`);
|
|
11877
11953
|
}
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
import { pathToFileURL } from "url";
|
|
11885
|
-
var RUNTIME_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
|
|
11886
|
-
var NODE_ENV_RUNTIME = {
|
|
11887
|
-
production: "prod",
|
|
11888
|
-
development: "dev",
|
|
11889
|
-
test: "local"
|
|
11890
|
-
};
|
|
11891
|
-
async function runRuntimeCommand(args) {
|
|
11892
|
-
const [action, ...rest] = args;
|
|
11893
|
-
if (action !== "prepare")
|
|
11894
|
-
throw new Error("runtime action must be prepare");
|
|
11895
|
-
const options = parse(rest);
|
|
11896
|
-
const root = path4.resolve(options.positionals[0] ?? process.cwd());
|
|
11897
|
-
const runtime = normalizedRuntime(option(options, "runtime") ?? process.env.SANDBLOCKS_RUNTIME ?? "development" ?? "development");
|
|
11898
|
-
const targetRoot = contained(root, option(options, "target") ?? ".locker", "runtime target");
|
|
11899
|
-
const runtimeRoot = path4.join(targetRoot, runtime);
|
|
11900
|
-
const config = contained(root, option(options, "config") ?? "config/dotlocker.config.ts", "Dotlocker config");
|
|
11901
|
-
const dotlocker = path4.resolve(root, option(options, "dotlocker") ?? path4.join("node_modules", ".bin", "dotlocker"));
|
|
11902
|
-
if (!await exists2(config))
|
|
11903
|
-
throw new Error(`Dotlocker config does not exist: ${config}`);
|
|
11904
|
-
if (!await exists2(dotlocker)) {
|
|
11905
|
-
throw new Error("Dotlocker is not installed; add @dotlocker/dotlocker to the workspace");
|
|
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;
|
|
11906
11960
|
}
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
throw new Error(`Dotlocker did not materialize ${path4.relative(root, envFile)}`);
|
|
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;
|
|
11913
11966
|
}
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11967
|
+
if (schema.parse)
|
|
11968
|
+
return schema.parse(value);
|
|
11969
|
+
throw new Error("structured output schema is unsupported");
|
|
11917
11970
|
}
|
|
11918
|
-
function
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
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;
|
|
11975
|
+
}
|
|
11976
|
+
function escapeRegex(value) {
|
|
11977
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11978
|
+
}
|
|
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"]
|
|
11991
|
+
});
|
|
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;
|
|
11927
12003
|
});
|
|
11928
12004
|
});
|
|
11929
|
-
|
|
11930
|
-
|
|
12005
|
+
child.stderr?.on("data", (chunk) => {
|
|
12006
|
+
const text = chunk.toString();
|
|
12007
|
+
stderr = `${stderr}${text}`.slice(-4 * 1024 * 1024);
|
|
12008
|
+
callbackQueue = callbackQueue.then(() => options.onStderr?.(text)).then(() => {
|
|
12009
|
+
return;
|
|
12010
|
+
});
|
|
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;
|
|
11931
12018
|
try {
|
|
11932
|
-
await
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
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);
|
|
11936
12038
|
}
|
|
11937
12039
|
}
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
throw new Error("Dotlocker output must not contain symbolic links");
|
|
11957
|
-
if (entry.isDirectory())
|
|
11958
|
-
await normalizePermissions(child);
|
|
11959
|
-
else if (entry.isFile())
|
|
11960
|
-
await chmod2(child, 420);
|
|
11961
|
-
else
|
|
11962
|
-
throw new Error("Dotlocker output contains an unsupported file type");
|
|
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;
|
|
11963
12058
|
}
|
|
11964
|
-
}
|
|
11965
|
-
|
|
11966
|
-
const options = { positionals: [] };
|
|
11967
|
-
for (let index = 0;index < args.length; index += 1) {
|
|
11968
|
-
const value = args[index] ?? "";
|
|
11969
|
-
if (!value.startsWith("--")) {
|
|
11970
|
-
options.positionals.push(value);
|
|
11971
|
-
continue;
|
|
11972
|
-
}
|
|
11973
|
-
const key = value.slice(2);
|
|
11974
|
-
const next = args[index + 1];
|
|
11975
|
-
if (next && !next.startsWith("--")) {
|
|
11976
|
-
options[key] = next;
|
|
11977
|
-
index += 1;
|
|
11978
|
-
} else
|
|
11979
|
-
options[key] = true;
|
|
12059
|
+
exec(command, options = {}) {
|
|
12060
|
+
return this.runtime.exec(command, options);
|
|
11980
12061
|
}
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
if (
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
if (command === "renew")
|
|
12026
|
-
return renew(args, dependencies);
|
|
12027
|
-
if (command === "exec")
|
|
12028
|
-
return exec(args, dependencies);
|
|
12029
|
-
if (command === "git")
|
|
12030
|
-
return git2(args, dependencies);
|
|
12031
|
-
return down(args, dependencies);
|
|
12032
|
-
}
|
|
12033
|
-
async function up(args, dependencies) {
|
|
12034
|
-
await withDeploymentLock(args, dependencies, async () => {
|
|
12035
|
-
await create(args, dependencies);
|
|
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);
|
|
12066
|
+
}
|
|
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);
|
|
12071
|
+
}
|
|
12072
|
+
stop(reason) {
|
|
12073
|
+
return this.runtime.stop(reason);
|
|
12074
|
+
}
|
|
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;
|
|
12083
|
+
}
|
|
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;
|
|
12095
|
+
}
|
|
12096
|
+
}
|
|
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 = [];
|
|
12036
12106
|
try {
|
|
12037
|
-
await
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
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
|
+
}
|
|
12042
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;
|
|
12043
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;
|
|
12044
12149
|
throw error;
|
|
12045
12150
|
}
|
|
12046
|
-
}
|
|
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);
|
|
12184
|
+
}
|
|
12047
12185
|
}
|
|
12048
|
-
async function
|
|
12049
|
-
const
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
const
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
const
|
|
12060
|
-
|
|
12061
|
-
const
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
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
|
|
12066
12222
|
});
|
|
12223
|
+
const sandbox = new SandblocksSandbox(runtime, options, git);
|
|
12067
12224
|
try {
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
ttlSeconds: Number(dependencies.option(options, "ttl-seconds") ?? 28800)
|
|
12074
|
-
})
|
|
12075
|
-
});
|
|
12076
|
-
const imported = await importSource({
|
|
12077
|
-
root,
|
|
12078
|
-
projectId,
|
|
12079
|
-
sandboxId,
|
|
12080
|
-
workspaceId,
|
|
12081
|
-
pool: dependencies.option(options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
|
|
12082
|
-
apiUrl,
|
|
12083
|
-
apiKey,
|
|
12084
|
-
idempotencyKey: `${sandboxId}:workspace-import`,
|
|
12085
|
-
source: dependencies.source,
|
|
12086
|
-
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
12087
|
-
});
|
|
12088
|
-
const hostId = String(imported.operation.assignedHostId ?? "");
|
|
12089
|
-
if (!hostId)
|
|
12090
|
-
throw new Error("workspace import did not bind a sandbox host");
|
|
12091
|
-
const state = {
|
|
12092
|
-
version: 1,
|
|
12093
|
-
environment,
|
|
12094
|
-
projectId,
|
|
12095
|
-
sandboxId,
|
|
12096
|
-
workspaceId,
|
|
12097
|
-
repository,
|
|
12098
|
-
leaseId: lease.lease.id,
|
|
12099
|
-
leaseToken: lease.token,
|
|
12100
|
-
hostId,
|
|
12101
|
-
sequence: 0,
|
|
12102
|
-
createdAt: new Date().toISOString()
|
|
12103
|
-
};
|
|
12104
|
-
await writeState(file, state);
|
|
12105
|
-
console.log(`sandbox ${sandboxId}`);
|
|
12106
|
-
console.log(`workspace ${workspaceId}`);
|
|
12107
|
-
console.log(`host ${hostId}`);
|
|
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;
|
|
12108
12230
|
} catch (error) {
|
|
12109
|
-
await
|
|
12110
|
-
method: "DELETE"
|
|
12111
|
-
}).catch(() => {
|
|
12231
|
+
await sandbox.destroy().catch(() => {
|
|
12112
12232
|
return;
|
|
12113
12233
|
});
|
|
12114
12234
|
throw error;
|
|
12115
12235
|
}
|
|
12116
12236
|
}
|
|
12117
|
-
async function
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
if (context.options.json) {
|
|
12124
|
-
console.log(JSON.stringify({ state: redactedState(context.state), sandbox: sandbox.sandbox, session: session.session }, null, 2));
|
|
12125
|
-
return;
|
|
12126
|
-
}
|
|
12127
|
-
console.log(`state ${path5.relative(context.root, context.file)}`);
|
|
12128
|
-
console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
|
|
12129
|
-
console.log(`workspace ${context.state.workspaceId}`);
|
|
12130
|
-
console.log(`host ${context.state.hostId}`);
|
|
12131
|
-
console.log(`lease expires ${session.session.expiresAt}`);
|
|
12132
|
-
for (const route of context.state.previewUrls ?? [])
|
|
12133
|
-
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
12134
|
-
}
|
|
12135
|
-
async function deploy(args, dependencies) {
|
|
12136
|
-
await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
|
|
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 });
|
|
12137
12243
|
}
|
|
12138
|
-
async function
|
|
12139
|
-
const
|
|
12140
|
-
const root = path5.resolve(options.positionals[0] ?? process.cwd());
|
|
12141
|
-
const environment = dependencies.option(options, "environment") ?? "preview";
|
|
12142
|
-
const lock = `${statePath(root, environment)}.deploy.lock`;
|
|
12143
|
-
await mkdir3(path5.dirname(lock), { recursive: true, mode: 448 });
|
|
12144
|
-
try {
|
|
12145
|
-
await mkdir3(lock);
|
|
12146
|
-
} catch (error) {
|
|
12147
|
-
if (error.code === "EEXIST") {
|
|
12148
|
-
throw new Error(`sandbox '${environment}' deployment is already running`);
|
|
12149
|
-
}
|
|
12150
|
-
throw error;
|
|
12151
|
-
}
|
|
12244
|
+
async function run(options) {
|
|
12245
|
+
const sandbox = await createSandbox(options);
|
|
12152
12246
|
try {
|
|
12153
|
-
await
|
|
12247
|
+
return await sandbox.run(options);
|
|
12154
12248
|
} finally {
|
|
12155
|
-
await
|
|
12249
|
+
await sandbox.destroy();
|
|
12156
12250
|
}
|
|
12157
12251
|
}
|
|
12158
|
-
async function
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
const
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
const
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
method: "POST",
|
|
12175
|
-
headers: {
|
|
12176
|
-
"idempotency-key": `preview:${deploymentId}:steps`
|
|
12177
|
-
},
|
|
12178
|
-
body: JSON.stringify({
|
|
12179
|
-
kind: "workspace.step.run",
|
|
12180
|
-
stack: context.state.repository,
|
|
12181
|
-
environmentId: context.state.environment,
|
|
12182
|
-
environmentSnapshotId,
|
|
12183
|
-
payload: {
|
|
12184
|
-
workspaceId: context.state.workspaceId,
|
|
12185
|
-
sandboxId: context.state.sandboxId,
|
|
12186
|
-
revisionId: deploymentId,
|
|
12187
|
-
runtimeType: preview.deploymentType,
|
|
12188
|
-
serviceUrls,
|
|
12189
|
-
steps: serviceSteps,
|
|
12190
|
-
placement: { hostId: context.state.hostId }
|
|
12191
|
-
},
|
|
12192
|
-
maxAttempts: 1
|
|
12193
|
-
})
|
|
12194
|
-
});
|
|
12195
|
-
await wait(context.apiUrl, context.apiKey, stepOperation.operation.id, 60 * 60000);
|
|
12252
|
+
async function resolvePrompt(options) {
|
|
12253
|
+
if (Boolean(options.prompt) === Boolean(options.promptFile))
|
|
12254
|
+
throw new Error("provide exactly one of prompt or promptFile");
|
|
12255
|
+
const promptFile = options.promptFile;
|
|
12256
|
+
let prompt = options.prompt ?? await readFile3(resolve(process.cwd(), promptFile ?? ""), "utf8");
|
|
12257
|
+
for (const [key, value] of Object.entries(options.promptArgs ?? {}))
|
|
12258
|
+
prompt = prompt.replaceAll(`{{${key}}}`, value);
|
|
12259
|
+
return prompt;
|
|
12260
|
+
}
|
|
12261
|
+
async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
|
|
12262
|
+
if (strategy.type === "head")
|
|
12263
|
+
return { hostCwd, runtimeCwd: hostCwd };
|
|
12264
|
+
const originalHead = await gitHead(hostCwd);
|
|
12265
|
+
if (strategy.type === "branch") {
|
|
12266
|
+
await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
|
|
12267
|
+
return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
|
|
12196
12268
|
}
|
|
12197
|
-
const
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12269
|
+
const branch = strategy.name ?? `sandblocks/${randomUUID().slice(0, 8)}`;
|
|
12270
|
+
const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
|
|
12271
|
+
await mkdir(dirname(worktree), { recursive: true });
|
|
12272
|
+
await checked(["git", "-C", hostCwd, "worktree", "add", "-B", branch, worktree, "HEAD"], timeoutMs);
|
|
12273
|
+
return {
|
|
12274
|
+
hostCwd,
|
|
12275
|
+
runtimeCwd: worktree,
|
|
12276
|
+
branch,
|
|
12277
|
+
worktree,
|
|
12278
|
+
mergeToHead: strategy.type === "merge-to-head",
|
|
12279
|
+
originalHead
|
|
12280
|
+
};
|
|
12281
|
+
}
|
|
12282
|
+
async function mergeBranch(git, timeoutMs = 60000) {
|
|
12283
|
+
if (!git.branch)
|
|
12284
|
+
throw new Error("merge branch is unavailable");
|
|
12285
|
+
await checked(["git", "-C", git.hostCwd, "merge", "--no-edit", git.branch], timeoutMs);
|
|
12286
|
+
}
|
|
12287
|
+
async function gitHead(cwd) {
|
|
12288
|
+
const result = await executeProcess(["git", "-C", cwd, "rev-parse", "HEAD"]);
|
|
12289
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
12290
|
+
}
|
|
12291
|
+
async function collectCommits(cwd, baseline, timeoutMs = 30000) {
|
|
12292
|
+
if (!baseline)
|
|
12293
|
+
return [];
|
|
12294
|
+
const result = await executeProcess(["git", "-C", cwd, "log", "--format=%H%x00%s", `${baseline}..HEAD`], {
|
|
12295
|
+
timeoutMs
|
|
12221
12296
|
});
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
headers: {
|
|
12245
|
-
"idempotency-key": `preview:${deploymentId}:checks`
|
|
12246
|
-
},
|
|
12247
|
-
body: JSON.stringify({
|
|
12248
|
-
kind: "workspace.check.run",
|
|
12249
|
-
stack: context.state.repository,
|
|
12250
|
-
environmentId: context.state.environment,
|
|
12251
|
-
environmentSnapshotId,
|
|
12252
|
-
payload: {
|
|
12253
|
-
workspaceId: context.state.workspaceId,
|
|
12254
|
-
deploymentId,
|
|
12255
|
-
sandboxId: context.state.sandboxId,
|
|
12256
|
-
runtimeType: preview.deploymentType,
|
|
12257
|
-
checks: preview.steps,
|
|
12258
|
-
serviceUrls: serviceUrls2,
|
|
12259
|
-
placement: { hostId: context.state.hostId }
|
|
12260
|
-
},
|
|
12261
|
-
maxAttempts: 1
|
|
12262
|
-
})
|
|
12263
|
-
});
|
|
12264
|
-
const checked = await wait(context.apiUrl, context.apiKey, check.operation.id, 60 * 60000);
|
|
12265
|
-
checkEvidence = checked.result?.checks ?? [];
|
|
12266
|
-
if (checked.result?.passed !== true) {
|
|
12267
|
-
throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
|
|
12268
|
-
}
|
|
12269
|
-
sandbox = await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "available");
|
|
12270
|
-
}
|
|
12271
|
-
} catch (error) {
|
|
12272
|
-
await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
|
|
12273
|
-
return;
|
|
12274
|
-
});
|
|
12275
|
-
await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
|
|
12276
|
-
return;
|
|
12297
|
+
if (result.exitCode !== 0 || !result.stdout.trim())
|
|
12298
|
+
return [];
|
|
12299
|
+
return result.stdout.trim().split(`
|
|
12300
|
+
`).map((line) => {
|
|
12301
|
+
const [sha, message] = line.split("\x00");
|
|
12302
|
+
if (!sha)
|
|
12303
|
+
throw new Error("Git returned an invalid commit record");
|
|
12304
|
+
return { sha, ...message ? { message } : {} };
|
|
12305
|
+
});
|
|
12306
|
+
}
|
|
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}`);
|
|
12311
|
+
}
|
|
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
|
|
12277
12319
|
});
|
|
12278
|
-
|
|
12320
|
+
if (result.exitCode !== 0)
|
|
12321
|
+
throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
|
|
12279
12322
|
}
|
|
12280
|
-
await releaseEnvironmentSnapshot(context, environmentSnapshotId);
|
|
12281
|
-
context.state.deploymentId = deploymentId;
|
|
12282
|
-
const previewUrls = sandbox.previewUrls ?? [];
|
|
12283
|
-
context.state.previewUrls = previewUrls;
|
|
12284
|
-
await writeState(context.file, context.state);
|
|
12285
|
-
console.log(`deployment ${deploymentId} available`);
|
|
12286
|
-
for (const route of previewUrls)
|
|
12287
|
-
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
12288
12323
|
}
|
|
12289
|
-
async function
|
|
12290
|
-
const
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...candidateState } = context.state;
|
|
12296
|
-
context.state = candidateState;
|
|
12297
|
-
await writeState(context.file, context.state);
|
|
12298
|
-
try {
|
|
12299
|
-
await destroyWorkspace(context, `${context.state.sandboxId}:workspace-redeploy-destroy:${Date.now()}`);
|
|
12300
|
-
const imported = await importSource({
|
|
12301
|
-
root: context.root,
|
|
12302
|
-
projectId: context.state.projectId,
|
|
12303
|
-
sandboxId: context.state.sandboxId,
|
|
12304
|
-
workspaceId: context.state.workspaceId,
|
|
12305
|
-
pool: dependencies.option(context.options, "pool") ?? process.env.SANDBLOCKS_WORKER_POOL ?? "sandbox-development",
|
|
12306
|
-
apiUrl: context.apiUrl,
|
|
12307
|
-
apiKey: context.apiKey,
|
|
12308
|
-
idempotencyKey: `${context.state.sandboxId}:workspace-redeploy:${Date.now()}`,
|
|
12309
|
-
source: dependencies.source,
|
|
12310
|
-
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
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
|
|
12311
12330
|
});
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
throw new Error("workspace re-import did not bind a sandbox host");
|
|
12315
|
-
context.state.hostId = hostId;
|
|
12316
|
-
await writeState(context.file, context.state);
|
|
12317
|
-
await deploy(args, dependencies);
|
|
12318
|
-
if (previousDeploymentId)
|
|
12319
|
-
await destroyDeploymentById(context, previousDeploymentId);
|
|
12320
|
-
} catch (error) {
|
|
12321
|
-
const failedCandidateId = context.state.deploymentId;
|
|
12322
|
-
if (failedCandidateId && failedCandidateId !== previousDeploymentId) {
|
|
12323
|
-
await destroyDeploymentById(context, failedCandidateId).catch(() => {
|
|
12324
|
-
return;
|
|
12325
|
-
});
|
|
12326
|
-
}
|
|
12327
|
-
context.state.deploymentId = previousDeploymentId;
|
|
12328
|
-
context.state.previewUrls = previousUrls;
|
|
12329
|
-
await writeState(context.file, context.state);
|
|
12330
|
-
throw error;
|
|
12331
|
+
if (result.exitCode !== 0)
|
|
12332
|
+
throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
|
|
12331
12333
|
}
|
|
12332
12334
|
}
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
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];
|
|
12337
12343
|
}
|
|
12338
|
-
|
|
12339
|
-
const
|
|
12340
|
-
const
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
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 {}
|
|
12360
|
+
};
|
|
12361
|
+
}
|
|
12362
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
12363
|
+
let timer;
|
|
12364
|
+
try {
|
|
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);
|
|
12374
|
+
}
|
|
12375
|
+
}
|
|
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`);
|
|
12386
|
+
}
|
|
12387
|
+
function array(value) {
|
|
12388
|
+
return Array.isArray(value) ? value : [value];
|
|
12389
|
+
}
|
|
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;
|
|
12433
|
+
}
|
|
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 })
|
|
12354
12466
|
});
|
|
12355
12467
|
}
|
|
12356
|
-
|
|
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));
|
|
12689
|
+
}
|
|
12690
|
+
throw new Error("Sandblocks operation timed out");
|
|
12691
|
+
}
|
|
12692
|
+
resourcePath(projectId, stack, app, suffix) {
|
|
12693
|
+
return `/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}${app ? `/apps/${encodeURIComponent(app)}` : ""}/${suffix}`;
|
|
12694
|
+
}
|
|
12695
|
+
secretPath(projectId, stack, app) {
|
|
12696
|
+
if (stack && app)
|
|
12697
|
+
return this.resourcePath(projectId, stack, app, "deployment-secrets");
|
|
12698
|
+
if (stack)
|
|
12699
|
+
return `/v1/projects/${encodeURIComponent(projectId)}/stacks/${encodeURIComponent(stack)}/deployment-secrets`;
|
|
12700
|
+
return `/v1/projects/${encodeURIComponent(projectId)}/deployment-secrets`;
|
|
12701
|
+
}
|
|
12702
|
+
async request(path3, init2 = {}) {
|
|
12703
|
+
const headers = new Headers(init2.headers);
|
|
12704
|
+
headers.set("x-sandblocks-api-key", this.options.apiKey);
|
|
12705
|
+
if (init2.body)
|
|
12706
|
+
headers.set("content-type", "application/json");
|
|
12707
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path3}`, { ...init2, headers });
|
|
12708
|
+
const body = await response.json().catch(() => ({}));
|
|
12709
|
+
if (!response.ok)
|
|
12710
|
+
throw new Error(body.error ?? `Sandblocks request failed (${response.status})`);
|
|
12711
|
+
return body;
|
|
12712
|
+
}
|
|
12357
12713
|
}
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
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
|
+
}
|
|
12366
12778
|
}
|
|
12367
|
-
async function
|
|
12368
|
-
const
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
if (
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
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
|
+
}
|
|
12378
12790
|
}
|
|
12379
|
-
async function
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
await destroyWorkspace(context, `${context.state.sandboxId}:workspace-destroy`).catch(() => {
|
|
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");
|
|
12383
12794
|
return;
|
|
12384
|
-
}
|
|
12385
|
-
|
|
12795
|
+
}
|
|
12796
|
+
const policy = (await dependencies.load(root)).manifest.hooks.postCommit;
|
|
12797
|
+
if (!policy?.enabled)
|
|
12386
12798
|
return;
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
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);
|
|
12390
12825
|
}
|
|
12391
|
-
async function
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
const body = await request(context.apiUrl, context.apiKey, "/v1/hosts");
|
|
12395
|
-
const host = Array.isArray(body.hosts) ? body.hosts.find((candidate) => candidate?.id === context.state.hostId) : undefined;
|
|
12396
|
-
const targetHost = host?.labels?.targetHost;
|
|
12397
|
-
return typeof targetHost === "string" && targetHost.trim() ? targetHost.trim() : undefined;
|
|
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;
|
|
12398
12829
|
}
|
|
12399
|
-
async function
|
|
12400
|
-
|
|
12401
|
-
if (!deploymentId)
|
|
12402
|
-
throw new Error("deployment ID is required when recording sandbox routes");
|
|
12403
|
-
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/sandboxes`, {
|
|
12404
|
-
method: "POST",
|
|
12405
|
-
body: JSON.stringify({
|
|
12406
|
-
id: context.state.sandboxId,
|
|
12407
|
-
goal: preview.goal,
|
|
12408
|
-
status: status3,
|
|
12409
|
-
stack: loaded.manifest.name ?? context.state.repository,
|
|
12410
|
-
deploymentType: preview.deploymentType,
|
|
12411
|
-
revisionId: deploymentId,
|
|
12412
|
-
generation: Date.now(),
|
|
12413
|
-
baseDomain: preview.baseDomain,
|
|
12414
|
-
domains: await managedServiceDomains(context, loaded.manifest.name ?? context.state.repository, preview, deploymentId),
|
|
12415
|
-
stableDomains: preview.stableDomains,
|
|
12416
|
-
serviceRoutes,
|
|
12417
|
-
healthEvidence,
|
|
12418
|
-
images,
|
|
12419
|
-
volumes: [
|
|
12420
|
-
{
|
|
12421
|
-
name: `${context.state.repository}-workspace`,
|
|
12422
|
-
target: "/workspace",
|
|
12423
|
-
access: "read-write"
|
|
12424
|
-
}
|
|
12425
|
-
],
|
|
12426
|
-
repositories: [context.state.repository],
|
|
12427
|
-
executor: "sandblocks"
|
|
12428
|
-
})
|
|
12429
|
-
});
|
|
12430
|
-
return body.sandbox;
|
|
12830
|
+
async function gitPath(root, name) {
|
|
12831
|
+
return path3.resolve(root, await git(root, ["rev-parse", "--git-path", name]));
|
|
12431
12832
|
}
|
|
12432
|
-
async function
|
|
12433
|
-
const
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
if (typeof id2 !== "string" || !/^[a-f0-9-]{36}$/.test(id2)) {
|
|
12443
|
-
throw new Error("managed environment snapshot response is invalid");
|
|
12444
|
-
}
|
|
12445
|
-
return id2;
|
|
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();
|
|
12446
12843
|
}
|
|
12447
|
-
async function
|
|
12448
|
-
|
|
12844
|
+
async function exists(file) {
|
|
12845
|
+
return Bun.file(file).exists();
|
|
12449
12846
|
}
|
|
12450
|
-
async function
|
|
12451
|
-
|
|
12847
|
+
async function text(file) {
|
|
12848
|
+
try {
|
|
12849
|
+
return await readFile4(file, "utf8");
|
|
12850
|
+
} catch {
|
|
12452
12851
|
return;
|
|
12453
|
-
|
|
12454
|
-
const { deploymentId: _deploymentId, previewUrls: _previewUrls, ...remainingState } = context.state;
|
|
12455
|
-
context.state = remainingState;
|
|
12456
|
-
await writeState(context.file, context.state);
|
|
12457
|
-
}
|
|
12458
|
-
async function destroyDeploymentById(context, deploymentId) {
|
|
12459
|
-
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
12460
|
-
method: "POST",
|
|
12461
|
-
headers: { "idempotency-key": `preview:${deploymentId}:destroy:${Date.now()}` },
|
|
12462
|
-
body: JSON.stringify({
|
|
12463
|
-
kind: "workspace.service.destroy",
|
|
12464
|
-
payload: { deploymentId, placement: { hostId: context.state.hostId } },
|
|
12465
|
-
maxAttempts: 1
|
|
12466
|
-
})
|
|
12467
|
-
});
|
|
12468
|
-
await wait(context.apiUrl, context.apiKey, body.operation.id);
|
|
12469
|
-
}
|
|
12470
|
-
async function destroyWorkspace(context, idempotencyKey) {
|
|
12471
|
-
const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}`, {
|
|
12472
|
-
method: "DELETE",
|
|
12473
|
-
headers: {
|
|
12474
|
-
"content-type": "application/json",
|
|
12475
|
-
"x-sandblocks-api-key": context.apiKey,
|
|
12476
|
-
"idempotency-key": idempotencyKey
|
|
12477
|
-
},
|
|
12478
|
-
body: JSON.stringify({ idempotencyKey })
|
|
12479
|
-
});
|
|
12480
|
-
const body = await response.json().catch(() => ({}));
|
|
12481
|
-
if (response.ok)
|
|
12482
|
-
await wait(context.apiUrl, context.apiKey, body.operation.id);
|
|
12483
|
-
else if (![404, 409].includes(response.status))
|
|
12484
|
-
throw new Error(String(body.error ?? "workspace destroy failed"));
|
|
12852
|
+
}
|
|
12485
12853
|
}
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
const
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12854
|
+
|
|
12855
|
+
// packages/libs/cli/src/runtime.ts
|
|
12856
|
+
import { spawn as spawn2 } from "child_process";
|
|
12857
|
+
import { access as access2, chmod as chmod2, mkdir as mkdir5, readdir, rm as rm3 } from "fs/promises";
|
|
12858
|
+
import path4 from "path";
|
|
12859
|
+
var RUNTIME_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
|
|
12860
|
+
var NODE_ENV_RUNTIME = {
|
|
12861
|
+
production: "prod",
|
|
12862
|
+
development: "dev",
|
|
12863
|
+
test: "local"
|
|
12864
|
+
};
|
|
12865
|
+
async function runRuntimeCommand(args) {
|
|
12866
|
+
const [action, ...rest] = args;
|
|
12867
|
+
if (action !== "prepare")
|
|
12868
|
+
throw new Error("runtime action must be prepare");
|
|
12869
|
+
const options = parse(rest);
|
|
12870
|
+
const root = path4.resolve(options.positionals[0] ?? process.cwd());
|
|
12871
|
+
const runtime = normalizedRuntime(option(options, "runtime") ?? process.env.SANDBLOCKS_RUNTIME ?? "development" ?? "development");
|
|
12872
|
+
const targetRoot = contained(root, option(options, "target") ?? ".locker", "runtime target");
|
|
12873
|
+
const runtimeRoot = path4.join(targetRoot, runtime);
|
|
12874
|
+
const config = contained(root, option(options, "config") ?? "config/dotlocker.config.ts", "Dotlocker config");
|
|
12875
|
+
const dotlocker = path4.resolve(root, option(options, "dotlocker") ?? path4.join("node_modules", ".bin", "dotlocker"));
|
|
12876
|
+
if (!await exists2(config))
|
|
12877
|
+
throw new Error(`Dotlocker config does not exist: ${config}`);
|
|
12878
|
+
if (!await exists2(dotlocker)) {
|
|
12879
|
+
throw new Error("Dotlocker is not installed; add @dotlocker/dotlocker to the workspace");
|
|
12880
|
+
}
|
|
12881
|
+
await rm3(targetRoot, { recursive: true, force: true });
|
|
12882
|
+
await mkdir5(runtimeRoot, { recursive: true, mode: 493 });
|
|
12883
|
+
await run2(dotlocker, ["pull", "--config", config, "--runtime", runtime, "--out", runtimeRoot], root);
|
|
12884
|
+
const envFile = path4.join(runtimeRoot, ".env");
|
|
12885
|
+
if (!await exists2(envFile)) {
|
|
12886
|
+
throw new Error(`Dotlocker did not materialize ${path4.relative(root, envFile)}`);
|
|
12887
|
+
}
|
|
12888
|
+
await normalizePermissions(targetRoot);
|
|
12889
|
+
console.log(`runtime ${runtime}
|
|
12890
|
+
environment ${path4.relative(root, envFile)}`);
|
|
12510
12891
|
}
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
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
|
+
});
|
|
12518
12902
|
});
|
|
12519
|
-
return waitLease(context, submitted.operation.id);
|
|
12520
12903
|
}
|
|
12521
|
-
async function
|
|
12522
|
-
const options = dependencies.parse(args);
|
|
12523
|
-
const root = path5.resolve(options.positionals[0] ?? process.cwd());
|
|
12524
|
-
await loadLocalEnvironment(root);
|
|
12525
|
-
const environment = dependencies.option(options, "environment") ?? "preview";
|
|
12526
|
-
const file = statePath(root, environment);
|
|
12527
|
-
let state;
|
|
12904
|
+
async function exists2(file) {
|
|
12528
12905
|
try {
|
|
12529
|
-
|
|
12906
|
+
await access2(file);
|
|
12907
|
+
return true;
|
|
12530
12908
|
} catch {
|
|
12531
|
-
|
|
12909
|
+
return false;
|
|
12910
|
+
}
|
|
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");
|
|
12532
12937
|
}
|
|
12533
|
-
const configured = client(options, dependencies.option, state.projectId);
|
|
12534
|
-
return { root, file, options, state, ...configured };
|
|
12535
12938
|
}
|
|
12536
|
-
function
|
|
12537
|
-
|
|
12939
|
+
function parse(args) {
|
|
12940
|
+
const options = { positionals: [] };
|
|
12941
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
12942
|
+
const value = args[index] ?? "";
|
|
12943
|
+
if (!value.startsWith("--")) {
|
|
12944
|
+
options.positionals.push(value);
|
|
12945
|
+
continue;
|
|
12946
|
+
}
|
|
12947
|
+
const key = value.slice(2);
|
|
12948
|
+
const next = args[index + 1];
|
|
12949
|
+
if (next && !next.startsWith("--")) {
|
|
12950
|
+
options[key] = next;
|
|
12951
|
+
index += 1;
|
|
12952
|
+
} else
|
|
12953
|
+
options[key] = true;
|
|
12954
|
+
}
|
|
12955
|
+
return options;
|
|
12538
12956
|
}
|
|
12539
|
-
function
|
|
12540
|
-
const
|
|
12541
|
-
|
|
12542
|
-
if (!apiUrl || !apiKey)
|
|
12543
|
-
throw new Error("SANDBLOCKS_API_URL and SANDBLOCKS_API_KEY are required");
|
|
12544
|
-
return { apiUrl, apiKey, projectId };
|
|
12957
|
+
function option(options, key) {
|
|
12958
|
+
const value = options[key];
|
|
12959
|
+
return typeof value === "string" ? value : undefined;
|
|
12545
12960
|
}
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
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));
|
|
12556
12981
|
}
|
|
12557
|
-
async function
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
if (
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
if (
|
|
12565
|
-
|
|
12566
|
-
|
|
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);
|
|
12567
13000
|
}
|
|
12568
|
-
async function
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
try {
|
|
12577
|
-
const response = await fetch(route.url, { redirect: "manual" });
|
|
12578
|
-
await response.body?.cancel().catch(() => {
|
|
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(() => {
|
|
12579
13009
|
return;
|
|
12580
13010
|
});
|
|
12581
|
-
return response.status >= 200 && response.status < 400 ? undefined : route;
|
|
12582
|
-
} catch {
|
|
12583
|
-
return route;
|
|
12584
13011
|
}
|
|
12585
|
-
|
|
12586
|
-
pending = checks.filter((route) => typeof route === "object" && route !== null);
|
|
12587
|
-
if (pending.length)
|
|
12588
|
-
await Bun.sleep(2000);
|
|
12589
|
-
}
|
|
12590
|
-
if (pending.length) {
|
|
12591
|
-
throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
|
|
12592
|
-
}
|
|
12593
|
-
}
|
|
12594
|
-
async function fetchWithRetry(url, init = {}) {
|
|
12595
|
-
const method = (init.method ?? "GET").toUpperCase();
|
|
12596
|
-
const headers = new Headers(init.headers);
|
|
12597
|
-
const safeToRetry = method === "GET" || method === "HEAD" || headers.has("idempotency-key");
|
|
12598
|
-
const attempts = safeToRetry ? 8 : 1;
|
|
12599
|
-
let lastError;
|
|
12600
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
12601
|
-
try {
|
|
12602
|
-
const response = await fetch(url, init);
|
|
12603
|
-
if (![409, 429, 502, 503, 504].includes(response.status) || attempt === attempts - 1)
|
|
12604
|
-
return response;
|
|
12605
|
-
await response.body?.cancel().catch(() => {
|
|
12606
|
-
return;
|
|
12607
|
-
});
|
|
12608
|
-
} catch (error) {
|
|
12609
|
-
lastError = error;
|
|
12610
|
-
if (attempt === attempts - 1)
|
|
12611
|
-
throw error;
|
|
13012
|
+
throw error;
|
|
12612
13013
|
}
|
|
12613
|
-
|
|
12614
|
-
}
|
|
12615
|
-
throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
|
|
12616
|
-
}
|
|
12617
|
-
async function wait(apiUrl, apiKey, operationId, timeoutMs = 30 * 60000) {
|
|
12618
|
-
const deadline = Date.now() + timeoutMs;
|
|
12619
|
-
while (Date.now() < deadline) {
|
|
12620
|
-
const body = await request(apiUrl, apiKey, `/v1/operations/${operationId}`);
|
|
12621
|
-
if (body.operation.state === "succeeded")
|
|
12622
|
-
return body.operation;
|
|
12623
|
-
if (["failed", "cancelled"].includes(body.operation.state))
|
|
12624
|
-
throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
|
|
12625
|
-
await Bun.sleep(500);
|
|
12626
|
-
}
|
|
12627
|
-
throw new Error(`operation ${operationId} timed out`);
|
|
12628
|
-
}
|
|
12629
|
-
async function waitLease(context, operationId) {
|
|
12630
|
-
const deadline = Date.now() + 3 * 60000;
|
|
12631
|
-
while (Date.now() < deadline) {
|
|
12632
|
-
const body = await leaseRequest(context, `/v1/sandbox-operations/${operationId}`);
|
|
12633
|
-
if (body.operation.state === "succeeded")
|
|
12634
|
-
return body.operation;
|
|
12635
|
-
if (["failed", "cancelled"].includes(body.operation.state))
|
|
12636
|
-
throw new Error(body.operation.failureReason ?? `operation ${body.operation.state}`);
|
|
12637
|
-
await Bun.sleep(500);
|
|
12638
|
-
}
|
|
12639
|
-
throw new Error(`sandbox operation ${operationId} timed out`);
|
|
13014
|
+
});
|
|
12640
13015
|
}
|
|
12641
|
-
async function
|
|
12642
|
-
const
|
|
12643
|
-
|
|
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
|
+
});
|
|
12644
13035
|
try {
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
const
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
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;
|
|
12657
13083
|
}
|
|
12658
13084
|
}
|
|
12659
|
-
async function
|
|
12660
|
-
const
|
|
12661
|
-
const
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
};
|
|
12672
|
-
}
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
return [];
|
|
12677
|
-
const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
12678
|
-
const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
|
|
12679
|
-
const configuredStableHosts = preview.stableDomains[service.id];
|
|
12680
|
-
const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.deploymentType}-${stack}-${service.id}.${preview.baseDomain}`;
|
|
12681
|
-
return [
|
|
12682
|
-
[`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
|
|
12683
|
-
[`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
|
|
12684
|
-
];
|
|
12685
|
-
}));
|
|
12686
|
-
}
|
|
12687
|
-
async function managedServiceDomains(context, stack, preview, deploymentId) {
|
|
12688
|
-
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`);
|
|
12689
|
-
const domains = body.policy.domains ?? {};
|
|
12690
|
-
return Object.keys(domains).length ? domains : resolvedServiceDomains(context.state.sandboxId, deploymentId, preview);
|
|
12691
|
-
}
|
|
12692
|
-
function resolvedServiceDomains(sandboxId, deploymentId, preview) {
|
|
12693
|
-
return Object.fromEntries(preview.services.map((service) => [
|
|
12694
|
-
service.id,
|
|
12695
|
-
resolvedServiceDomain(sandboxId, deploymentId, service.id, preview)
|
|
12696
|
-
]));
|
|
12697
|
-
}
|
|
12698
|
-
function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
|
|
12699
|
-
const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${deploymentId}/${service}`).digest("hex").slice(0, 8)}`;
|
|
12700
|
-
const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
|
|
12701
|
-
return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
|
|
12702
|
-
}
|
|
12703
|
-
function statePath(root, environment) {
|
|
12704
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(environment))
|
|
12705
|
-
throw new Error("sandbox environment is invalid");
|
|
12706
|
-
return path5.join(root, ".sandblocks", `sandbox-${environment}.json`);
|
|
12707
|
-
}
|
|
12708
|
-
async function writeState(file, state) {
|
|
12709
|
-
await mkdir3(path5.dirname(file), { recursive: true, mode: 448 });
|
|
12710
|
-
await chmod3(path5.dirname(file), 448);
|
|
12711
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
12712
|
-
await Bun.write(temporary, `${JSON.stringify(state, null, 2)}
|
|
12713
|
-
`, { mode: 384 });
|
|
12714
|
-
await chmod3(temporary, 384);
|
|
12715
|
-
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}`);
|
|
12716
13102
|
}
|
|
12717
|
-
function
|
|
12718
|
-
|
|
13103
|
+
async function deploy(args, dependencies) {
|
|
13104
|
+
await withDeploymentLock(args, dependencies, () => deployUnlocked(args, dependencies));
|
|
12719
13105
|
}
|
|
12720
|
-
function
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
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
|
+
}
|
|
12727
13125
|
}
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
options;
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
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);
|
|
12747
13160
|
}
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
|
|
12751
|
-
const promptMode = this.options.prompt ?? "stdin";
|
|
12752
|
-
const command = promptMode === "argument" ? [...base, context.prompt] : base;
|
|
12753
|
-
const idle = new AbortController;
|
|
12754
|
-
const completed = new AbortController;
|
|
12755
|
-
let idleTimer;
|
|
12756
|
-
let completionTimer;
|
|
12757
|
-
const resetIdle = () => {
|
|
12758
|
-
if (idleTimer)
|
|
12759
|
-
clearTimeout(idleTimer);
|
|
12760
|
-
idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
|
|
12761
|
-
};
|
|
12762
|
-
resetIdle();
|
|
12763
|
-
let output = "";
|
|
12764
|
-
let stdout = "";
|
|
12765
|
-
let stderr = "";
|
|
12766
|
-
let lineBuffer = "";
|
|
12767
|
-
let completionSignal;
|
|
12768
|
-
const consume = async (chunk, stream) => {
|
|
12769
|
-
resetIdle();
|
|
12770
|
-
output += chunk;
|
|
12771
|
-
if (stream === "stdout")
|
|
12772
|
-
stdout += chunk;
|
|
12773
|
-
else
|
|
12774
|
-
stderr += chunk;
|
|
12775
|
-
lineBuffer += chunk;
|
|
12776
|
-
const lines = lineBuffer.split(/\r?\n/);
|
|
12777
|
-
lineBuffer = lines.pop() ?? "";
|
|
12778
|
-
for (const line of lines)
|
|
12779
|
-
await this.emitLine(line, stream, context);
|
|
12780
|
-
completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
|
|
12781
|
-
if (completionSignal && !completionTimer) {
|
|
12782
|
-
completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
|
|
12783
|
-
}
|
|
12784
|
-
};
|
|
13161
|
+
if (!artifactImage && serviceSteps.length) {
|
|
13162
|
+
buildWorkspaceId ??= `${context.state.sandboxId.slice(0, 8)}-${context.state.repository}-build-${deploymentId.slice(0, 8)}`.slice(0, 63);
|
|
12785
13163
|
try {
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
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 } : {}
|
|
12794
13175
|
});
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
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);
|
|
12802
13211
|
}
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
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`" : ""}`);
|
|
12810
13255
|
}
|
|
12811
13256
|
}
|
|
12812
|
-
|
|
12813
|
-
const
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
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 }) });
|
|
12824
13324
|
}
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
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)}`);
|
|
12834
13363
|
}
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
command: (context) => [
|
|
12848
|
-
options.binary ?? "claude",
|
|
12849
|
-
"--print",
|
|
12850
|
-
"--model",
|
|
12851
|
-
options.model,
|
|
12852
|
-
...options.effort ? ["--effort", options.effort] : [],
|
|
12853
|
-
...options.extraArgs ?? []
|
|
12854
|
-
],
|
|
12855
|
-
sessionId: extractSessionId,
|
|
12856
|
-
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;
|
|
12857
13376
|
});
|
|
13377
|
+
throw error;
|
|
12858
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}`);
|
|
12859
13390
|
}
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
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 } : {}
|
|
12877
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;
|
|
12878
13437
|
}
|
|
12879
13438
|
}
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
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;
|
|
12897
13460
|
});
|
|
12898
13461
|
}
|
|
13462
|
+
return lease;
|
|
12899
13463
|
}
|
|
12900
|
-
function
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
}
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
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;
|
|
12918
13549
|
}
|
|
12919
|
-
function
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
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;
|
|
12931
13564
|
}
|
|
12932
|
-
function
|
|
12933
|
-
|
|
12934
|
-
${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
|
|
12935
|
-
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" });
|
|
12936
13567
|
}
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
return
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
};
|
|
12945
|
-
async function extractOutput(spec, text2) {
|
|
12946
|
-
const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
|
|
12947
|
-
const match = text2.match(pattern);
|
|
12948
|
-
if (!match)
|
|
12949
|
-
throw new Error(`structured output tag <${spec.tag}> was not found`);
|
|
12950
|
-
const content = match[1]?.trim();
|
|
12951
|
-
if (content === undefined)
|
|
12952
|
-
throw new Error(`structured output tag <${spec.tag}> is empty`);
|
|
12953
|
-
if (spec.kind === "string")
|
|
12954
|
-
return content;
|
|
12955
|
-
let value;
|
|
12956
|
-
try {
|
|
12957
|
-
value = JSON.parse(content);
|
|
12958
|
-
} catch {
|
|
12959
|
-
throw new Error(`structured output <${spec.tag}> is not valid JSON`);
|
|
12960
|
-
}
|
|
12961
|
-
const schema = spec.schema;
|
|
12962
|
-
if (schema["~standard"]) {
|
|
12963
|
-
const result = await schema["~standard"].validate(value);
|
|
12964
|
-
if (result.issues?.length)
|
|
12965
|
-
throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
|
|
12966
|
-
return result.value;
|
|
12967
|
-
}
|
|
12968
|
-
if (schema.safeParse) {
|
|
12969
|
-
const result = schema.safeParse(value);
|
|
12970
|
-
if (!result.success)
|
|
12971
|
-
throw new Error(`structured output validation failed: ${String(result.error)}`);
|
|
12972
|
-
return result.data;
|
|
12973
|
-
}
|
|
12974
|
-
if (schema.parse)
|
|
12975
|
-
return schema.parse(value);
|
|
12976
|
-
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);
|
|
12977
13575
|
}
|
|
12978
|
-
function
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
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);
|
|
12982
13590
|
}
|
|
12983
|
-
function
|
|
12984
|
-
|
|
13591
|
+
async function destroyWorkspace(context, idempotencyKey) {
|
|
13592
|
+
await destroyDetachedWorkspace(context, context.state.workspaceId, idempotencyKey);
|
|
12985
13593
|
}
|
|
12986
|
-
async function
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
cwd: options.cwd ?? options.hostCwd,
|
|
12996
|
-
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
12997
|
-
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 })
|
|
12998
13603
|
});
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
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)])
|
|
13011
13625
|
});
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
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)
|
|
13018
13642
|
});
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
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;
|
|
13025
13652
|
try {
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
});
|
|
13030
|
-
await callbackQueue;
|
|
13031
|
-
if (options.signal?.aborted)
|
|
13032
|
-
throw options.signal.reason ?? new Error("operation aborted");
|
|
13033
|
-
return {
|
|
13034
|
-
exitCode: timedOut ? 124 : exitCode,
|
|
13035
|
-
stdout,
|
|
13036
|
-
stderr,
|
|
13037
|
-
timedOut,
|
|
13038
|
-
startedAt,
|
|
13039
|
-
finishedAt: new Date().toISOString()
|
|
13040
|
-
};
|
|
13041
|
-
} finally {
|
|
13042
|
-
if (timer)
|
|
13043
|
-
clearTimeout(timer);
|
|
13044
|
-
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`);
|
|
13045
13656
|
}
|
|
13657
|
+
const configured = client(options, dependencies.option, state.projectId);
|
|
13658
|
+
return { root, file, options, state, ...configured };
|
|
13046
13659
|
}
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
}
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
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);
|
|
13078
13713
|
}
|
|
13079
|
-
|
|
13080
|
-
|
|
13714
|
+
if (pending.length) {
|
|
13715
|
+
throw new Error(`preview routes did not become ready: ${pending.map((route) => route.label).join(", ")}`);
|
|
13081
13716
|
}
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
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(() => {
|
|
13085
13730
|
return;
|
|
13086
|
-
const queued = this.eventQueue.shift();
|
|
13087
|
-
if (queued) {
|
|
13088
|
-
yield queued;
|
|
13089
|
-
continue;
|
|
13090
|
-
}
|
|
13091
|
-
const event = await new Promise((resolve22) => {
|
|
13092
|
-
const abort = () => resolve22(undefined);
|
|
13093
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
13094
|
-
this.eventWaiters.push((value) => {
|
|
13095
|
-
options.signal?.removeEventListener("abort", abort);
|
|
13096
|
-
resolve22(value);
|
|
13097
|
-
});
|
|
13098
13731
|
});
|
|
13099
|
-
if (!event)
|
|
13100
|
-
return;
|
|
13101
|
-
yield event;
|
|
13102
|
-
}
|
|
13103
|
-
}
|
|
13104
|
-
async run(options) {
|
|
13105
|
-
if (options.output && (options.maxIterations ?? 1) !== 1)
|
|
13106
|
-
throw new Error("structured output requires maxIterations === 1");
|
|
13107
|
-
const startedAt = new Date().toISOString();
|
|
13108
|
-
const prompt = await resolvePrompt(options);
|
|
13109
|
-
const signals = array(options.completionSignal ?? "<promise>COMPLETE</promise>");
|
|
13110
|
-
const logger = createLogger(options.logging, this.git.hostCwd);
|
|
13111
|
-
const baseline = await gitHead(this.git.runtimeCwd);
|
|
13112
|
-
const iterations = [];
|
|
13113
|
-
try {
|
|
13114
|
-
await runSandboxHooks(this.runtime, options.hooks?.sandbox?.beforeAgent);
|
|
13115
|
-
for (let iteration = 1;iteration <= (options.maxIterations ?? 1); iteration += 1) {
|
|
13116
|
-
const result = await options.agent.run({
|
|
13117
|
-
runtime: this.runtime,
|
|
13118
|
-
prompt,
|
|
13119
|
-
iteration,
|
|
13120
|
-
signal: options.signal,
|
|
13121
|
-
idleTimeoutSeconds: options.idleTimeoutSeconds ?? 600,
|
|
13122
|
-
completionTimeoutSeconds: options.completionTimeoutSeconds ?? 60,
|
|
13123
|
-
completionSignal: signals,
|
|
13124
|
-
emit: async (event) => {
|
|
13125
|
-
this.publish(event);
|
|
13126
|
-
await logger(event);
|
|
13127
|
-
}
|
|
13128
|
-
});
|
|
13129
|
-
iterations.push(result);
|
|
13130
|
-
if (result.exitCode !== 0)
|
|
13131
|
-
throw new Error(result.stderr || `${options.agent.id} exited ${result.exitCode}`);
|
|
13132
|
-
if (result.completionSignal)
|
|
13133
|
-
break;
|
|
13134
|
-
}
|
|
13135
|
-
await runSandboxHooks(this.runtime, options.hooks?.sandbox?.afterAgent);
|
|
13136
|
-
await this.runtime.syncToHost?.();
|
|
13137
|
-
const commits = await collectCommits(this.git.runtimeCwd, baseline, this.options.timeouts?.commitCollectionMs);
|
|
13138
|
-
if (this.git.mergeToHead && this.git.branch)
|
|
13139
|
-
await mergeBranch(this.git, this.options.timeouts?.mergeToHostMs);
|
|
13140
|
-
const allOutput = iterations.map((item) => item.stdout).join(`
|
|
13141
|
-
`);
|
|
13142
|
-
const completionSignal = iterations.find((item) => item.completionSignal)?.completionSignal;
|
|
13143
|
-
return {
|
|
13144
|
-
sandboxId: this.id,
|
|
13145
|
-
provider: this.provider,
|
|
13146
|
-
iterations,
|
|
13147
|
-
commits,
|
|
13148
|
-
...this.git.branch ? { branch: this.git.branch } : {},
|
|
13149
|
-
...completionSignal ? { completionSignal } : {},
|
|
13150
|
-
...options.output ? { output: await extractOutput(options.output, allOutput) } : {},
|
|
13151
|
-
startedAt,
|
|
13152
|
-
finishedAt: new Date().toISOString()
|
|
13153
|
-
};
|
|
13154
13732
|
} catch (error) {
|
|
13155
|
-
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
}
|
|
13159
|
-
async destroy() {
|
|
13160
|
-
if (this.disposed)
|
|
13161
|
-
return;
|
|
13162
|
-
this.disposed = true;
|
|
13163
|
-
await runHostHooks(this.options.hooks?.host?.beforeDestroy, this.git.hostCwd);
|
|
13164
|
-
const retention = this.options.retention ?? "destroy";
|
|
13165
|
-
if (retention === "destroy" || retention === "on-failure" && !this.failed)
|
|
13166
|
-
await this.runtime.destroy();
|
|
13167
|
-
for (const waiter of this.eventWaiters.splice(0))
|
|
13168
|
-
waiter(undefined);
|
|
13169
|
-
if (this.git.worktree && retention !== "always") {
|
|
13170
|
-
await executeProcess([
|
|
13171
|
-
"git",
|
|
13172
|
-
"-C",
|
|
13173
|
-
this.git.hostCwd,
|
|
13174
|
-
"worktree",
|
|
13175
|
-
"remove",
|
|
13176
|
-
"--force",
|
|
13177
|
-
this.git.worktree
|
|
13178
|
-
]);
|
|
13179
|
-
await rm4(this.git.worktree, { recursive: true, force: true });
|
|
13733
|
+
lastError = error;
|
|
13734
|
+
if (attempt === attempts - 1)
|
|
13735
|
+
throw error;
|
|
13180
13736
|
}
|
|
13737
|
+
await Bun.sleep(Math.min(5000, 250 * 2 ** attempt));
|
|
13181
13738
|
}
|
|
13182
|
-
|
|
13183
|
-
await this.destroy();
|
|
13184
|
-
}
|
|
13185
|
-
publish(event) {
|
|
13186
|
-
const waiter = this.eventWaiters.shift();
|
|
13187
|
-
if (waiter)
|
|
13188
|
-
waiter(event);
|
|
13189
|
-
else
|
|
13190
|
-
this.eventQueue.push(event);
|
|
13191
|
-
}
|
|
13739
|
+
throw lastError instanceof Error ? lastError : new Error("Sandblocks request failed");
|
|
13192
13740
|
}
|
|
13193
|
-
async function
|
|
13194
|
-
const
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13202
|
-
throw new Error("copyToWorktree is not supported with the head branch strategy");
|
|
13203
|
-
}
|
|
13204
|
-
const git3 = await prepareGit(hostCwd, branchStrategy, options.timeouts?.gitSetupMs);
|
|
13205
|
-
await runHostHooks(options.hooks?.host?.onWorktreeReady, git3.runtimeCwd);
|
|
13206
|
-
for (const file of options.copyToWorktree ?? []) {
|
|
13207
|
-
const source = resolve(hostCwd, file);
|
|
13208
|
-
const destination = resolve(git3.runtimeCwd, file);
|
|
13209
|
-
await mkdir4(dirname(destination), { recursive: true });
|
|
13210
|
-
await withTimeout(cp(source, destination, { recursive: true }), options.timeouts?.copyToWorktreeMs ?? 60000, `copying '${file}'`);
|
|
13211
|
-
}
|
|
13212
|
-
const id2 = options.id ?? randomUUID2();
|
|
13213
|
-
const runtime = await provider.create({
|
|
13214
|
-
id: id2,
|
|
13215
|
-
cwd: git3.runtimeCwd,
|
|
13216
|
-
image: options.image,
|
|
13217
|
-
env: options.env,
|
|
13218
|
-
mounts: options.mounts,
|
|
13219
|
-
network: options.network,
|
|
13220
|
-
groups: options.groups,
|
|
13221
|
-
devices: options.devices,
|
|
13222
|
-
cpus: options.cpus,
|
|
13223
|
-
memoryMb: options.memoryMb,
|
|
13224
|
-
pids: options.pids,
|
|
13225
|
-
tmpfsMb: options.tmpfsMb,
|
|
13226
|
-
signal: options.signal,
|
|
13227
|
-
idempotencyKey: options.idempotencyKey,
|
|
13228
|
-
metadata: options.metadata
|
|
13229
|
-
});
|
|
13230
|
-
const sandbox = new SandblocksSandbox(runtime, options, git3);
|
|
13231
|
-
try {
|
|
13232
|
-
await runHostHooks(options.hooks?.host?.onSandboxReady, hostCwd);
|
|
13233
|
-
await runHostHooks(options.hooks?.host?.afterCreate, hostCwd);
|
|
13234
|
-
await runSandboxHooks(runtime, options.hooks?.sandbox?.onSandboxReady);
|
|
13235
|
-
await runSandboxHooks(runtime, options.hooks?.sandbox?.afterCreate);
|
|
13236
|
-
return sandbox;
|
|
13237
|
-
} catch (error) {
|
|
13238
|
-
await sandbox.destroy().catch(() => {
|
|
13239
|
-
return;
|
|
13240
|
-
});
|
|
13241
|
-
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);
|
|
13242
13750
|
}
|
|
13751
|
+
throw new Error(`operation ${operationId} timed out`);
|
|
13243
13752
|
}
|
|
13244
|
-
async function
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
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`);
|
|
13250
13764
|
}
|
|
13251
|
-
async function
|
|
13252
|
-
const
|
|
13765
|
+
async function loadLocalEnvironment(root) {
|
|
13766
|
+
const file = path5.join(root, ".sandblocks", "config.env");
|
|
13767
|
+
let contents;
|
|
13253
13768
|
try {
|
|
13254
|
-
|
|
13255
|
-
}
|
|
13256
|
-
|
|
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, "");
|
|
13257
13781
|
}
|
|
13258
13782
|
}
|
|
13259
|
-
async function
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
const
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
return prompt;
|
|
13267
|
-
}
|
|
13268
|
-
async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
|
|
13269
|
-
if (strategy.type === "head")
|
|
13270
|
-
return { hostCwd, runtimeCwd: hostCwd };
|
|
13271
|
-
const originalHead = await gitHead(hostCwd);
|
|
13272
|
-
if (strategy.type === "branch") {
|
|
13273
|
-
await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
|
|
13274
|
-
return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
|
|
13275
|
-
}
|
|
13276
|
-
const branch = strategy.name ?? `sandblocks/${randomUUID2().slice(0, 8)}`;
|
|
13277
|
-
const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
|
|
13278
|
-
await mkdir4(dirname(worktree), { recursive: true });
|
|
13279
|
-
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
|
+
}));
|
|
13280
13790
|
return {
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
mergeToHead: strategy.type === "merge-to-head",
|
|
13286
|
-
originalHead
|
|
13791
|
+
...preview,
|
|
13792
|
+
baseDomain: policy.baseDomain ?? preview.baseDomain,
|
|
13793
|
+
domains: { ...preview.domains, ...primaryDomains },
|
|
13794
|
+
stableDomains: { ...preview.stableDomains, ...policy.stableDomains }
|
|
13287
13795
|
};
|
|
13288
13796
|
}
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
timeoutMs
|
|
13303
|
-
});
|
|
13304
|
-
if (result.exitCode !== 0 || !result.stdout.trim())
|
|
13305
|
-
return [];
|
|
13306
|
-
return result.stdout.trim().split(`
|
|
13307
|
-
`).map((line) => {
|
|
13308
|
-
const [sha, message] = line.split("\x00");
|
|
13309
|
-
if (!sha)
|
|
13310
|
-
throw new Error("Git returned an invalid commit record");
|
|
13311
|
-
return { sha, ...message ? { message } : {} };
|
|
13312
|
-
});
|
|
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
|
+
}));
|
|
13313
13810
|
}
|
|
13314
|
-
async function
|
|
13315
|
-
const
|
|
13316
|
-
|
|
13317
|
-
|
|
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);
|
|
13318
13815
|
}
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
env: hook.env,
|
|
13325
|
-
timeoutMs: hook.timeoutMs
|
|
13326
|
-
});
|
|
13327
|
-
if (result.exitCode !== 0)
|
|
13328
|
-
throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
|
|
13329
|
-
}
|
|
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
|
+
]));
|
|
13330
13821
|
}
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
env: hook.env,
|
|
13336
|
-
timeoutMs: hook.timeoutMs
|
|
13337
|
-
});
|
|
13338
|
-
if (result.exitCode !== 0)
|
|
13339
|
-
throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
|
|
13340
|
-
}
|
|
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();
|
|
13341
13826
|
}
|
|
13342
|
-
function
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
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);
|
|
13350
13835
|
}
|
|
13351
|
-
function
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
return
|
|
13355
|
-
const line = `${JSON.stringify(event)}
|
|
13356
|
-
`;
|
|
13357
|
-
if (config.type === "stdout")
|
|
13358
|
-
process.stdout.write(event.type === "text" ? event.text : event.type === "raw" ? `${event.line}
|
|
13359
|
-
` : line);
|
|
13360
|
-
if (config.type !== "stdout" && config.type !== "silent") {
|
|
13361
|
-
await mkdir4(dirname(file), { recursive: true });
|
|
13362
|
-
await appendFile(file, line);
|
|
13363
|
-
}
|
|
13364
|
-
try {
|
|
13365
|
-
await config.onAgentStreamEvent?.(event);
|
|
13366
|
-
} catch {}
|
|
13367
|
-
};
|
|
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`);
|
|
13368
13840
|
}
|
|
13369
|
-
async function
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
]);
|
|
13378
|
-
} finally {
|
|
13379
|
-
if (timer)
|
|
13380
|
-
clearTimeout(timer);
|
|
13381
|
-
}
|
|
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);
|
|
13382
13849
|
}
|
|
13383
|
-
function
|
|
13384
|
-
|
|
13385
|
-
if (options.mounts?.length && !capabilities.bindMounts)
|
|
13386
|
-
throw new Error(`${provider.kind} provider does not support bind mounts`);
|
|
13387
|
-
if (options.network && !capabilities.networks)
|
|
13388
|
-
throw new Error(`${provider.kind} provider does not support network selection`);
|
|
13389
|
-
if (options.devices?.length && !capabilities.devices)
|
|
13390
|
-
throw new Error(`${provider.kind} provider does not support devices`);
|
|
13391
|
-
if ((options.retention === "always" || options.retention === "on-failure") && !capabilities.persistent)
|
|
13392
|
-
throw new Error(`${provider.kind} provider does not support retained sandboxes`);
|
|
13850
|
+
function redactedState(state) {
|
|
13851
|
+
return { ...state, leaseToken: "<redacted>" };
|
|
13393
13852
|
}
|
|
13394
|
-
function
|
|
13395
|
-
|
|
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}`);
|
|
13396
13860
|
}
|
|
13397
|
-
async function initSdk(options = {}) {
|
|
13398
|
-
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
13399
|
-
const root = resolve2(cwd, ".sandblocks");
|
|
13400
|
-
const files = {
|
|
13401
|
-
"main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
|
|
13402
|
-
import { DockerProvider } from "@sandblocks/sdk/providers/docker";
|
|
13403
|
-
|
|
13404
|
-
await using sandbox = await createSandbox({
|
|
13405
|
-
provider: new DockerProvider({ image: "node:22-bookworm" }),
|
|
13406
|
-
cwd: process.cwd(),
|
|
13407
|
-
});
|
|
13408
|
-
|
|
13409
|
-
const result = await sandbox.run({
|
|
13410
|
-
agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
|
|
13411
|
-
promptFile: ".sandblocks/prompts/implement.md",
|
|
13412
|
-
logging: { type: "stdout" },
|
|
13413
|
-
});
|
|
13414
|
-
|
|
13415
|
-
console.log(result);
|
|
13416
|
-
`,
|
|
13417
|
-
"prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
|
|
13418
13861
|
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
CLAUDE_CODE_OAUTH_TOKEN=
|
|
13423
|
-
# ANTHROPIC_API_KEY=
|
|
13424
|
-
SANDBLOCKS_API_URL=
|
|
13425
|
-
SANDBLOCKS_API_KEY=
|
|
13426
|
-
`,
|
|
13427
|
-
"config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
|
|
13428
|
-
`
|
|
13429
|
-
};
|
|
13430
|
-
const written = [];
|
|
13431
|
-
for (const [relative, content] of Object.entries(files)) {
|
|
13432
|
-
const path6 = resolve2(root, relative);
|
|
13433
|
-
await mkdir22(resolve2(path6, ".."), { recursive: true });
|
|
13434
|
-
if (!options.force && await access2(path6).then(() => true).catch(() => false))
|
|
13435
|
-
continue;
|
|
13436
|
-
await writeFile2(path6, content, { mode: relative.includes(".env") ? 384 : 420 });
|
|
13437
|
-
written.push(path6);
|
|
13438
|
-
}
|
|
13439
|
-
return written;
|
|
13440
|
-
}
|
|
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";
|
|
13441
13865
|
|
|
13442
|
-
//
|
|
13866
|
+
// packages/libs/sdk/dist/providers/docker.js
|
|
13443
13867
|
import { realpath as realpath3 } from "fs/promises";
|
|
13444
13868
|
import { resolve as resolve3 } from "path";
|
|
13445
13869
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -13685,7 +14109,7 @@ class DockerProvider extends OciProvider {
|
|
|
13685
14109
|
}
|
|
13686
14110
|
}
|
|
13687
14111
|
|
|
13688
|
-
//
|
|
14112
|
+
// packages/libs/sdk/dist/providers/podman.js
|
|
13689
14113
|
import { realpath as realpath4 } from "fs/promises";
|
|
13690
14114
|
import { resolve as resolve5 } from "path";
|
|
13691
14115
|
import { spawn as spawn4 } from "child_process";
|
|
@@ -13931,7 +14355,7 @@ class PodmanProvider extends OciProvider2 {
|
|
|
13931
14355
|
}
|
|
13932
14356
|
}
|
|
13933
14357
|
|
|
13934
|
-
//
|
|
14358
|
+
// packages/libs/sdk/dist/providers/remote.js
|
|
13935
14359
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
13936
14360
|
import { mkdtemp, readFile as readFile6, rm as rm5, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
13937
14361
|
import { tmpdir } from "os";
|
|
@@ -14255,7 +14679,7 @@ async function sourceBundle(cwd) {
|
|
|
14255
14679
|
}
|
|
14256
14680
|
}
|
|
14257
14681
|
|
|
14258
|
-
//
|
|
14682
|
+
// packages/libs/sdk/dist/providers/unsafe-host.js
|
|
14259
14683
|
import { cp as cp2, realpath as realpath5 } from "fs/promises";
|
|
14260
14684
|
import { resolve as resolve6 } from "path";
|
|
14261
14685
|
import { spawn as spawn6 } from "child_process";
|
|
@@ -14364,7 +14788,7 @@ class UnsafeHostProvider {
|
|
|
14364
14788
|
}
|
|
14365
14789
|
}
|
|
14366
14790
|
|
|
14367
|
-
// src/sdk.ts
|
|
14791
|
+
// packages/libs/cli/src/sdk.ts
|
|
14368
14792
|
async function runSdkCommand(args, helpers) {
|
|
14369
14793
|
const [command = "help", ...rest] = args;
|
|
14370
14794
|
const parsed = helpers.parse(rest);
|
|
@@ -14380,7 +14804,7 @@ async function runSdkCommand(args, helpers) {
|
|
|
14380
14804
|
const provider = providerFrom(options, helpers, cwd);
|
|
14381
14805
|
const agent = agentFrom(options, helpers);
|
|
14382
14806
|
const outputTag = helpers.option(options, "output-tag");
|
|
14383
|
-
const result = await
|
|
14807
|
+
const result = await run({
|
|
14384
14808
|
provider,
|
|
14385
14809
|
cwd,
|
|
14386
14810
|
image: helpers.option(options, "image"),
|
|
@@ -14431,7 +14855,7 @@ async function sdkSandbox(args, helpers, defaultCwd) {
|
|
|
14431
14855
|
hooks: hooksFrom(options),
|
|
14432
14856
|
copyToWorktree: multi(options["copy-to-worktree"])
|
|
14433
14857
|
});
|
|
14434
|
-
await
|
|
14858
|
+
await mkdir7(dirname2(stateFile), { recursive: true });
|
|
14435
14859
|
const strategy = branchFrom(options, helpers);
|
|
14436
14860
|
const worktree = strategy.type === "worktree" || strategy.type === "merge-to-head" ? sandbox2.cwd : undefined;
|
|
14437
14861
|
await writeFile4(stateFile, JSON.stringify({
|
|
@@ -14678,14 +15102,14 @@ function parseMount(value) {
|
|
|
14678
15102
|
return { source, target, readonly: mode === "ro" };
|
|
14679
15103
|
}
|
|
14680
15104
|
|
|
14681
|
-
// src/cli.ts
|
|
15105
|
+
// packages/libs/cli/src/cli.ts
|
|
14682
15106
|
var HELP = `Sandblocks CLI
|
|
14683
15107
|
|
|
14684
15108
|
sandblocks configure [directory] [--name <stack>] [--force]
|
|
14685
15109
|
sandblocks validate [directory] [--config <path>] [--json]
|
|
14686
15110
|
sandblocks register [directory] --project <id> [--repository-url <url>]
|
|
14687
15111
|
[--api-url <url>] [--api-key <key>] [--json]
|
|
14688
|
-
sandblocks doctor [directory] [--api-url <url>]
|
|
15112
|
+
sandblocks doctor [directory] [--api-url <url>] [--explain-placement] [--project <id>] [--json]
|
|
14689
15113
|
sandblocks whoami [directory] [--api-url <url>] [--api-key <key>] [--json]
|
|
14690
15114
|
sandblocks hooks <install|status|uninstall> [directory]
|
|
14691
15115
|
sandblocks hooks run post-commit [directory]
|
|
@@ -14695,10 +15119,12 @@ var HELP = `Sandblocks CLI
|
|
|
14695
15119
|
sandblocks app <list|get|settings|settings-set> --project <id> [--stack <id>] [--app <id>] [--file <json>] [--json]
|
|
14696
15120
|
sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
|
|
14697
15121
|
[--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
|
|
14698
|
-
sandblocks
|
|
15122
|
+
sandblocks artifact push <directory> --project <id> --stack <id> [--pool <pool>] [--json]
|
|
15123
|
+
sandblocks sandbox up [directory] [--environment <id>] [--project <id>] [--build-pool <pool>]
|
|
14699
15124
|
sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
|
|
14700
|
-
sandblocks sandbox deploy [directory] [--environment <id>]
|
|
14701
|
-
|
|
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>]
|
|
14702
15128
|
sandblocks sandbox status [directory] [--environment <id>] [--json]
|
|
14703
15129
|
sandblocks sandbox promote --project <id> --sandbox <id> [--json]
|
|
14704
15130
|
sandblocks sandbox agent <run|list|get|cancel|redeploy> --project <id> --sandbox <id>
|
|
@@ -14765,6 +15191,8 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
14765
15191
|
await resource(command, args);
|
|
14766
15192
|
else if (command === "secret")
|
|
14767
15193
|
await secret(args);
|
|
15194
|
+
else if (command === "artifact")
|
|
15195
|
+
await artifact(args);
|
|
14768
15196
|
else if (command === "sandbox")
|
|
14769
15197
|
await sandbox(args);
|
|
14770
15198
|
else if (command === "sdk")
|
|
@@ -14865,6 +15293,38 @@ async function doctor(args) {
|
|
|
14865
15293
|
if (!response.ok)
|
|
14866
15294
|
throw new Error(`API health failed (${response.status})`);
|
|
14867
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
|
+
}
|
|
14868
15328
|
}
|
|
14869
15329
|
const gitDetected = await stat3(path6.join(root, ".git")).then(() => true).catch(() => false);
|
|
14870
15330
|
console.log(`git ${gitDetected ? "ok" : "not detected"}`);
|
|
@@ -14980,6 +15440,45 @@ async function secret(args) {
|
|
|
14980
15440
|
}
|
|
14981
15441
|
}
|
|
14982
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
|
+
}
|
|
14983
15482
|
function printResources(kind, action, body) {
|
|
14984
15483
|
if (action === "settings") {
|
|
14985
15484
|
console.log(JSON.stringify(body.settings ?? {}, null, 2));
|
|
@@ -15149,6 +15648,51 @@ async function sandboxPromote(args) {
|
|
|
15149
15648
|
console.log(`promoted ${sandboxId} to production`);
|
|
15150
15649
|
}
|
|
15151
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
|
+
}
|
|
15152
15696
|
async function createSourceTar(root) {
|
|
15153
15697
|
const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
15154
15698
|
cwd: root,
|
|
@@ -15459,4 +16003,4 @@ export {
|
|
|
15459
16003
|
parse2 as parse
|
|
15460
16004
|
};
|
|
15461
16005
|
|
|
15462
|
-
//# debugId=
|
|
16006
|
+
//# debugId=E7F4E37BF2F49E8E64756E2164756E21
|