@clipboard-health/groundcrew 2.2.0 → 2.3.1
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/README.md +72 -8
- package/configExample.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -10
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +10 -35
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -13
- package/dist/commands/eligibility.d.ts +14 -0
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +44 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +23 -4
- package/dist/commands/ticketDoctor.d.ts +48 -0
- package/dist/commands/ticketDoctor.d.ts.map +1 -0
- package/dist/commands/ticketDoctor.js +402 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/boardSource.d.ts +55 -0
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +171 -26
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +75 -7
- package/dist/lib/dockerSandbox.d.ts +40 -0
- package/dist/lib/dockerSandbox.d.ts.map +1 -0
- package/dist/lib/dockerSandbox.js +58 -0
- package/dist/lib/host.d.ts +10 -0
- package/dist/lib/host.d.ts.map +1 -1
- package/dist/lib/host.js +8 -3
- package/dist/lib/launchCommand.d.ts +17 -3
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +66 -8
- package/dist/lib/localRunner.d.ts +22 -1
- package/dist/lib/localRunner.d.ts.map +1 -1
- package/dist/lib/localRunner.js +48 -5
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +138 -40
- package/package.json +1 -1
package/dist/lib/boardSource.js
CHANGED
|
@@ -129,9 +129,9 @@ async function fetchBoard(client, config) {
|
|
|
129
129
|
const issues = nodes
|
|
130
130
|
.filter((node) => node.children.nodes.length === 0)
|
|
131
131
|
.map((node) => {
|
|
132
|
-
const
|
|
133
|
-
warnIfDisabledFallback(node.identifier,
|
|
134
|
-
const repository =
|
|
132
|
+
const modelResolution = resolveModelFor({ labels: node.labels.nodes, config });
|
|
133
|
+
warnIfDisabledFallback(node.identifier, modelResolution, config);
|
|
134
|
+
const repository = modelResolution.kind === "no-label"
|
|
135
135
|
? undefined
|
|
136
136
|
: parseRepository({
|
|
137
137
|
description: node.description ?? undefined,
|
|
@@ -139,6 +139,16 @@ async function fetchBoard(client, config) {
|
|
|
139
139
|
repositoryRegex,
|
|
140
140
|
ticket: node.identifier,
|
|
141
141
|
});
|
|
142
|
+
let model;
|
|
143
|
+
if (modelResolution.kind === "matched") {
|
|
144
|
+
({ model } = modelResolution);
|
|
145
|
+
}
|
|
146
|
+
else if (modelResolution.kind === "disabled-fallback") {
|
|
147
|
+
model = modelResolution.fallbackModel;
|
|
148
|
+
}
|
|
149
|
+
else if (modelResolution.kind === "agent-any") {
|
|
150
|
+
model = AGENT_ANY_MODEL;
|
|
151
|
+
}
|
|
142
152
|
return {
|
|
143
153
|
id: node.identifier.toLowerCase(),
|
|
144
154
|
uuid: node.id,
|
|
@@ -148,7 +158,7 @@ async function fetchBoard(client, config) {
|
|
|
148
158
|
assignee: node.assignee?.name ?? "Unassigned",
|
|
149
159
|
updatedAt: node.updatedAt,
|
|
150
160
|
repository,
|
|
151
|
-
model
|
|
161
|
+
model,
|
|
152
162
|
teamId: node.team?.id ?? "",
|
|
153
163
|
blockers: blockersFromRelations(node.inverseRelations?.nodes ?? []),
|
|
154
164
|
hasMoreBlockers: node.inverseRelations?.pageInfo.hasNextPage ?? false,
|
|
@@ -174,22 +184,64 @@ function buildRepositoryRegex(config) {
|
|
|
174
184
|
return new RegExp(String.raw `\b(${alternation})\b`);
|
|
175
185
|
}
|
|
176
186
|
const ISSUE_LABEL_PAGE_SIZE = 50;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
const ISSUE_RELATION_PAGE_SIZE = 50;
|
|
188
|
+
export async function fetchBlockersForTicket(arguments_) {
|
|
189
|
+
const { client, uuid } = arguments_;
|
|
190
|
+
const relations = [];
|
|
191
|
+
let after = null;
|
|
192
|
+
for (;;) {
|
|
193
|
+
// oxlint-disable-next-line no-await-in-loop -- pagination cursor depends on the previous response
|
|
194
|
+
const response = await client.client.rawRequest(`query IssueBlockers($id: String!, $after: String) {
|
|
195
|
+
issue(id: $id) {
|
|
196
|
+
inverseRelations(first: ${ISSUE_RELATION_PAGE_SIZE}, after: $after, includeArchived: false) {
|
|
197
|
+
nodes {
|
|
198
|
+
type
|
|
199
|
+
issue {
|
|
200
|
+
identifier
|
|
201
|
+
title
|
|
202
|
+
state { name }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
pageInfo { hasNextPage endCursor }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}`, { id: uuid, after });
|
|
209
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- shape is fixed by our GraphQL query above
|
|
210
|
+
const { issue } = response.data;
|
|
211
|
+
if (issue === null) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
relations.push(...issue.inverseRelations.nodes);
|
|
215
|
+
if (!issue.inverseRelations.pageInfo.hasNextPage) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
after = issue.inverseRelations.pageInfo.endCursor;
|
|
219
|
+
}
|
|
220
|
+
return blockersFromRelations(relations);
|
|
221
|
+
}
|
|
222
|
+
export async function fetchRawLinearIssue(arguments_) {
|
|
223
|
+
const { client, ticket } = arguments_;
|
|
184
224
|
const response = await client.client.rawRequest(`query ResolveIssue($id: String!) {
|
|
185
225
|
issue(id: $id) {
|
|
186
226
|
id
|
|
187
227
|
title
|
|
188
228
|
description
|
|
189
229
|
team { id }
|
|
230
|
+
state { name }
|
|
190
231
|
labels(first: ${ISSUE_LABEL_PAGE_SIZE}) {
|
|
191
232
|
nodes { name }
|
|
192
233
|
}
|
|
234
|
+
inverseRelations(first: 50, includeArchived: false) {
|
|
235
|
+
nodes {
|
|
236
|
+
type
|
|
237
|
+
issue {
|
|
238
|
+
identifier
|
|
239
|
+
title
|
|
240
|
+
state { name }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
pageInfo { hasNextPage }
|
|
244
|
+
}
|
|
193
245
|
}
|
|
194
246
|
}`, { id: ticket.toUpperCase() });
|
|
195
247
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- shape is fixed by our GraphQL query above
|
|
@@ -197,26 +249,119 @@ export async function fetchResolvedIssue(arguments_) {
|
|
|
197
249
|
if (issue === null) {
|
|
198
250
|
throw new Error(`Ticket ${ticket.toUpperCase()} not found in Linear`);
|
|
199
251
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
252
|
+
return {
|
|
253
|
+
uuid: issue.id,
|
|
254
|
+
title: issue.title,
|
|
255
|
+
description: issue.description ?? "",
|
|
256
|
+
teamId: issue.team?.id ?? "",
|
|
257
|
+
labels: issue.labels.nodes,
|
|
258
|
+
stateName: issue.state?.name ?? "",
|
|
259
|
+
blockers: blockersFromRelations(issue.inverseRelations?.nodes ?? []),
|
|
260
|
+
hasMoreBlockers: issue.inverseRelations?.pageInfo.hasNextPage ?? false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function fetchInProgressIssueCount(arguments_) {
|
|
264
|
+
const { client, config } = arguments_;
|
|
265
|
+
let after = null;
|
|
266
|
+
let count = 0;
|
|
267
|
+
for (;;) {
|
|
268
|
+
// oxlint-disable-next-line no-await-in-loop -- pagination cursor depends on the previous response
|
|
269
|
+
const response = await client.client.rawRequest(`query InProgressIssues($slugId: String!, $stateName: String!, $agentLabelPrefix: String!, $after: String) {
|
|
270
|
+
issues(
|
|
271
|
+
filter: {
|
|
272
|
+
project: { slugId: { eq: $slugId } }
|
|
273
|
+
state: { name: { eq: $stateName } }
|
|
274
|
+
labels: { some: { name: { startsWith: $agentLabelPrefix } } }
|
|
275
|
+
}
|
|
276
|
+
first: ${ISSUES_PAGE_SIZE}
|
|
277
|
+
after: $after
|
|
278
|
+
includeArchived: false
|
|
279
|
+
) {
|
|
280
|
+
nodes { id }
|
|
281
|
+
pageInfo { hasNextPage endCursor }
|
|
282
|
+
}
|
|
283
|
+
}`, {
|
|
284
|
+
slugId: config.linear.slugId,
|
|
285
|
+
stateName: config.linear.statuses.inProgress,
|
|
286
|
+
agentLabelPrefix: AGENT_LABEL_PREFIX,
|
|
287
|
+
after,
|
|
288
|
+
});
|
|
289
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- shape is fixed by our GraphQL query above
|
|
290
|
+
const { issues: page } = response.data;
|
|
291
|
+
count += page.nodes.length;
|
|
292
|
+
if (!page.pageInfo.hasNextPage) {
|
|
293
|
+
return count;
|
|
294
|
+
}
|
|
295
|
+
after = page.pageInfo.endCursor;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export function resolveRepositoryFor(arguments_) {
|
|
299
|
+
const { description, config } = arguments_;
|
|
300
|
+
if (description === undefined || description.length === 0) {
|
|
301
|
+
return { kind: "missing" };
|
|
302
|
+
}
|
|
303
|
+
const repository = buildRepositoryRegex(config).exec(description)?.[1];
|
|
304
|
+
if (repository === undefined) {
|
|
305
|
+
return { kind: "missing" };
|
|
306
|
+
}
|
|
307
|
+
return { kind: "ok", repository };
|
|
308
|
+
}
|
|
309
|
+
export function resolveModelFor(arguments_) {
|
|
310
|
+
const { labels, config } = arguments_;
|
|
311
|
+
const parsed = parseAgentLabels(labels, config);
|
|
312
|
+
if (parsed === undefined) {
|
|
313
|
+
return { kind: "no-label" };
|
|
314
|
+
}
|
|
315
|
+
if (parsed.model === AGENT_ANY_MODEL) {
|
|
316
|
+
return { kind: "agent-any" };
|
|
317
|
+
}
|
|
318
|
+
if (parsed.disabledFallback !== undefined) {
|
|
319
|
+
return {
|
|
320
|
+
kind: "disabled-fallback",
|
|
321
|
+
requestedModel: parsed.disabledFallback,
|
|
322
|
+
fallbackModel: parsed.model,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { kind: "matched", model: parsed.model };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `agent-any` collapses to `models.default` here — manual setup doesn't run
|
|
329
|
+
* the usage-gated `any` resolver, so the caller gets a concrete model name
|
|
330
|
+
* instead of a sentinel that downstream code can't interpret.
|
|
331
|
+
*/
|
|
332
|
+
export async function fetchResolvedIssue(arguments_) {
|
|
333
|
+
const { client, config, ticket } = arguments_;
|
|
334
|
+
const raw = await fetchRawLinearIssue({ client, ticket });
|
|
335
|
+
const repositoryResolution = resolveRepositoryFor({
|
|
336
|
+
description: raw.description,
|
|
203
337
|
config,
|
|
204
|
-
repositoryRegex: buildRepositoryRegex(config),
|
|
205
338
|
ticket: ticket.toUpperCase(),
|
|
206
339
|
});
|
|
340
|
+
if (repositoryResolution.kind === "missing") {
|
|
341
|
+
throw new RepositoryResolutionError({
|
|
342
|
+
ticket: ticket.toUpperCase(),
|
|
343
|
+
repositories: config.workspace.knownRepositories,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
207
346
|
// Manual setup is an explicit per-ticket opt-in by the user, so an
|
|
208
347
|
// unlabeled ticket still resolves to `models.default` — different from
|
|
209
348
|
// the auto-pickup path, where unlabeled tickets are ignored.
|
|
210
|
-
const
|
|
211
|
-
warnIfDisabledFallback(ticket,
|
|
212
|
-
|
|
349
|
+
const modelResolution = resolveModelFor({ labels: raw.labels, config });
|
|
350
|
+
warnIfDisabledFallback(ticket, modelResolution, config);
|
|
351
|
+
let model = config.models.default;
|
|
352
|
+
if (modelResolution.kind === "matched") {
|
|
353
|
+
({ model } = modelResolution);
|
|
354
|
+
}
|
|
355
|
+
else if (modelResolution.kind === "disabled-fallback") {
|
|
356
|
+
model = modelResolution.fallbackModel;
|
|
357
|
+
}
|
|
213
358
|
return {
|
|
214
|
-
uuid:
|
|
215
|
-
title:
|
|
216
|
-
description,
|
|
217
|
-
repository,
|
|
359
|
+
uuid: raw.uuid,
|
|
360
|
+
title: raw.title,
|
|
361
|
+
description: raw.description,
|
|
362
|
+
repository: repositoryResolution.repository,
|
|
218
363
|
model,
|
|
219
|
-
teamId:
|
|
364
|
+
teamId: raw.teamId,
|
|
220
365
|
};
|
|
221
366
|
}
|
|
222
367
|
function parseRepository(arguments_) {
|
|
@@ -278,11 +423,11 @@ function parseAgentLabels(labels, config) {
|
|
|
278
423
|
}
|
|
279
424
|
return fallback;
|
|
280
425
|
}
|
|
281
|
-
function warnIfDisabledFallback(ticket,
|
|
282
|
-
if (
|
|
426
|
+
function warnIfDisabledFallback(ticket, modelResolution, config) {
|
|
427
|
+
if (modelResolution.kind !== "disabled-fallback") {
|
|
283
428
|
return;
|
|
284
429
|
}
|
|
285
|
-
log(`${ticket.toLowerCase()}: agent-${
|
|
430
|
+
log(`${ticket.toLowerCase()}: agent-${modelResolution.requestedModel} label refers to a disabled model; falling back to models.default (${config.models.default})`);
|
|
286
431
|
}
|
|
287
432
|
function blockersFromRelations(relations) {
|
|
288
433
|
return relations
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -15,6 +15,39 @@ export declare const AGENT_ANY_MODEL = "any";
|
|
|
15
15
|
*/
|
|
16
16
|
export type WorkspaceKindSetting = "auto" | "cmux" | "tmux";
|
|
17
17
|
export declare const WORKSPACE_KIND_SETTINGS: readonly WorkspaceKindSetting[];
|
|
18
|
+
/**
|
|
19
|
+
* Concrete local isolation backend selected for a launch. `safehouse` is
|
|
20
|
+
* macOS-only (clearance HTTP-egress + sandbox profile); `sdx` is Docker
|
|
21
|
+
* Sandboxes (`sbx` CLI) — works on Linux and macOS and is the only known
|
|
22
|
+
* option that lets the agent use Docker safely without exposing the host
|
|
23
|
+
* socket; `none` is an explicit unsandboxed escape hatch.
|
|
24
|
+
*/
|
|
25
|
+
export type LocalRunner = "safehouse" | "sdx" | "none";
|
|
26
|
+
/**
|
|
27
|
+
* User-facing local runner setting. `auto` resolves at launch time:
|
|
28
|
+
* macOS picks `safehouse`, Linux picks `sdx`. `none` is never picked
|
|
29
|
+
* implicitly.
|
|
30
|
+
*/
|
|
31
|
+
export type LocalRunnerSetting = LocalRunner | "auto";
|
|
32
|
+
export declare const LOCAL_RUNNER_SETTINGS: readonly LocalRunnerSetting[];
|
|
33
|
+
/**
|
|
34
|
+
* Per-model Docker Sandboxes (sdx) binding. Required at launch when
|
|
35
|
+
* `local.runner` resolves to `sdx` so groundcrew knows which sbx agent
|
|
36
|
+
* to address and how to seed the sandbox.
|
|
37
|
+
*/
|
|
38
|
+
export interface SandboxDefinition {
|
|
39
|
+
/** sbx agent name (e.g. "claude", "codex"). */
|
|
40
|
+
agent: string;
|
|
41
|
+
/** Optional `sbx run --template` value. */
|
|
42
|
+
template?: string;
|
|
43
|
+
/** Optional `sbx run --kit` values (each passed as a separate flag). */
|
|
44
|
+
kits?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Setup command run **inside** the sandbox before the agent exec.
|
|
47
|
+
* Defaults to `DEFAULT_SANDBOX_SETUP_COMMAND` when omitted.
|
|
48
|
+
*/
|
|
49
|
+
setupCommand?: string;
|
|
50
|
+
}
|
|
18
51
|
export interface ModelDefinition {
|
|
19
52
|
/**
|
|
20
53
|
* Shell command launched for the model. Wrapped with Safehouse/clearance
|
|
@@ -32,6 +65,12 @@ export interface ModelDefinition {
|
|
|
32
65
|
source?: string;
|
|
33
66
|
};
|
|
34
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Docker Sandboxes binding. Required when `local.runner` resolves to
|
|
70
|
+
* `sdx` — pure additive: omitted models can still run under `safehouse`
|
|
71
|
+
* or `none` without surprise.
|
|
72
|
+
*/
|
|
73
|
+
sandbox?: SandboxDefinition;
|
|
35
74
|
}
|
|
36
75
|
/**
|
|
37
76
|
* User-facing model entry shape. Discriminated union so the type system
|
|
@@ -51,6 +90,14 @@ type UserModelDefinition = EnabledUserModelDefinition | DisabledUserModelDefinit
|
|
|
51
90
|
* assumed to already have the right Node and npm versions.
|
|
52
91
|
*/
|
|
53
92
|
export declare const DEFAULT_HOST_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
93
|
+
/**
|
|
94
|
+
* Setup command run inside an sdx (Docker Sandboxes) sandbox before the
|
|
95
|
+
* agent process exec. Independent of the host setup — sandboxes typically
|
|
96
|
+
* lack Node tooling on first start, so we keep the recipe scoped to the
|
|
97
|
+
* common case of an npm-managed repo while still letting per-model
|
|
98
|
+
* `sandbox.setupCommand` override it for languages outside that path.
|
|
99
|
+
*/
|
|
100
|
+
export declare const DEFAULT_SANDBOX_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
54
101
|
/**
|
|
55
102
|
* Loose user-facing shape — what a `config.ts` file declares.
|
|
56
103
|
* Fields with defaults are optional; only `linear.projectSlug` and the
|
|
@@ -107,6 +154,14 @@ export interface Config {
|
|
|
107
154
|
* to fail loudly when the chosen backend is missing.
|
|
108
155
|
*/
|
|
109
156
|
workspaceKind?: WorkspaceKindSetting;
|
|
157
|
+
/**
|
|
158
|
+
* Local isolation backend selector. Defaults to `"auto"` (macOS →
|
|
159
|
+
* safehouse, Linux → sdx). `"none"` is an explicit unsandboxed escape
|
|
160
|
+
* hatch — never selected implicitly.
|
|
161
|
+
*/
|
|
162
|
+
local?: {
|
|
163
|
+
runner?: LocalRunnerSetting;
|
|
164
|
+
};
|
|
110
165
|
logging?: {
|
|
111
166
|
/**
|
|
112
167
|
* Append-mode log file destination. `log()` and `logEvent()` tee here
|
|
@@ -158,6 +213,14 @@ export interface ResolvedConfig {
|
|
|
158
213
|
* `auto` resolves to cmux when installed, else tmux.
|
|
159
214
|
*/
|
|
160
215
|
workspaceKind: WorkspaceKindSetting;
|
|
216
|
+
/**
|
|
217
|
+
* Local isolation selection. The user-facing `auto` is preserved here
|
|
218
|
+
* so `localRunner.resolve()` can pick the platform default later — the
|
|
219
|
+
* resolver is the only place that knows the host capabilities.
|
|
220
|
+
*/
|
|
221
|
+
local: {
|
|
222
|
+
runner: LocalRunnerSetting;
|
|
223
|
+
};
|
|
161
224
|
logging: {
|
|
162
225
|
file: string;
|
|
163
226
|
};
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAIzD,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtD,eAAO,MAAM,qBAAqB,EAAE,SAAS,kBAAkB,EAKrD,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,CAAC;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;;GAKG;AACH,KAAK,0BAA0B,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAClF,UAAU,2BAA2B;IACnC,QAAQ,EAAE,IAAI,CAAC;CAChB;AACD,KAAK,mBAAmB,GAAG,0BAA0B,GAAG,2BAA2B,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,yMACiK,CAAC;AAEzM;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,yMAC8J,CAAC;AAEzM;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE;QACN;;;;;;;;WAQG;QACH,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE;YACT,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;SACrB,CAAC;KACH,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;KACjC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;;;;;WAKG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;KACnD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;;OAIG;IACH,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,kBAAkB,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACR;;;;;WAKG;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE;QACN,2EAA2E;QAC3E,WAAW,EAAE,MAAM,CAAC;QACpB,uEAAuE;QACvE,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE;YACR,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,MAAM,CAAC;YACnB,IAAI,EAAE,MAAM,CAAC;YACb,QAAQ,EAAE,MAAM,EAAE,CAAC;SACpB,CAAC;KACH,CAAC;IACF,GAAG,EAAE;QACH,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,YAAY,EAAE;QACZ,iBAAiB,EAAE,MAAM,CAAC;QAC1B,wBAAwB,EAAE,MAAM,CAAC;QACjC,sBAAsB,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;KAC9C,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF;;;OAGG;IACH,aAAa,EAAE,oBAAoB,CAAC;IACpC;;;;OAIG;IACH,KAAK,EAAE;QACL,MAAM,EAAE,kBAAkB,CAAC;KAC5B,CAAC;IACF,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AA4RD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAKT;AAoPD,wBAAsB,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CA8BpE"}
|
package/dist/lib/config.js
CHANGED
|
@@ -17,11 +17,25 @@ export const WORKSPACE_KIND_SETTINGS = [
|
|
|
17
17
|
"cmux",
|
|
18
18
|
"tmux",
|
|
19
19
|
];
|
|
20
|
+
export const LOCAL_RUNNER_SETTINGS = [
|
|
21
|
+
"auto",
|
|
22
|
+
"safehouse",
|
|
23
|
+
"sdx",
|
|
24
|
+
"none",
|
|
25
|
+
];
|
|
20
26
|
/**
|
|
21
27
|
* Setup command run inside sibling worktrees on the host. The host is
|
|
22
28
|
* assumed to already have the right Node and npm versions.
|
|
23
29
|
*/
|
|
24
30
|
export const DEFAULT_HOST_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
31
|
+
/**
|
|
32
|
+
* Setup command run inside an sdx (Docker Sandboxes) sandbox before the
|
|
33
|
+
* agent process exec. Independent of the host setup — sandboxes typically
|
|
34
|
+
* lack Node tooling on first start, so we keep the recipe scoped to the
|
|
35
|
+
* common case of an npm-managed repo while still letting per-model
|
|
36
|
+
* `sandbox.setupCommand` override it for languages outside that path.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_SANDBOX_SETUP_COMMAND = "if [ -x .claude/setup.sh ]; then ./.claude/setup.sh --deps-only; elif [ -f .claude/setup.sh ] && command -v bash >/dev/null 2>&1; then bash .claude/setup.sh --deps-only; else npm clean-install; fi";
|
|
25
39
|
const DEFAULT_STATUSES = {
|
|
26
40
|
todo: "Todo",
|
|
27
41
|
inProgress: "In Progress",
|
|
@@ -194,6 +208,43 @@ function normalizeWorkspaceKind(value, path) {
|
|
|
194
208
|
}
|
|
195
209
|
return value;
|
|
196
210
|
}
|
|
211
|
+
function isLocalRunnerSetting(value) {
|
|
212
|
+
return typeof value === "string" && LOCAL_RUNNER_SETTINGS.includes(value);
|
|
213
|
+
}
|
|
214
|
+
function normalizeLocalRunner(value, path) {
|
|
215
|
+
if (value === undefined) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
if (!isLocalRunnerSetting(value)) {
|
|
219
|
+
fail(`${path} must be one of ${LOCAL_RUNNER_SETTINGS.join(", ")} (got ${JSON.stringify(value)})`);
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function normalizeSandbox(value, path) {
|
|
224
|
+
if (!isPlainObject(value)) {
|
|
225
|
+
fail(`${path} must be an object`);
|
|
226
|
+
}
|
|
227
|
+
const { agent, template, kits, setupCommand } = value;
|
|
228
|
+
requireString(agent, `${path}.agent`);
|
|
229
|
+
const trimmedAgent = agent.trim();
|
|
230
|
+
if (trimmedAgent.length === 0) {
|
|
231
|
+
fail(`${path}.agent must be a non-empty string (got ${JSON.stringify(agent)})`);
|
|
232
|
+
}
|
|
233
|
+
const sandbox = { agent: trimmedAgent };
|
|
234
|
+
const normalizedTemplate = normalizeOptionalString(template, `${path}.template`);
|
|
235
|
+
if (normalizedTemplate !== undefined) {
|
|
236
|
+
sandbox.template = normalizedTemplate;
|
|
237
|
+
}
|
|
238
|
+
const normalizedKits = normalizeOptionalStringArray(kits, `${path}.kits`);
|
|
239
|
+
if (normalizedKits !== undefined) {
|
|
240
|
+
sandbox.kits = normalizedKits;
|
|
241
|
+
}
|
|
242
|
+
const normalizedSetup = normalizeOptionalString(setupCommand, `${path}.setupCommand`);
|
|
243
|
+
if (normalizedSetup !== undefined) {
|
|
244
|
+
sandbox.setupCommand = normalizedSetup;
|
|
245
|
+
}
|
|
246
|
+
return sandbox;
|
|
247
|
+
}
|
|
197
248
|
function failIfLegacyModelKeys(name, override) {
|
|
198
249
|
if (!isPlainObject(override)) {
|
|
199
250
|
fail(`models.definitions.${name} must be an object`);
|
|
@@ -201,14 +252,11 @@ function failIfLegacyModelKeys(name, override) {
|
|
|
201
252
|
if (Object.hasOwn(override, "isolation")) {
|
|
202
253
|
fail(`models.definitions.${name}.isolation is no longer supported: per-model isolation is no longer supported`);
|
|
203
254
|
}
|
|
204
|
-
if (Object.hasOwn(override, "sandbox")) {
|
|
205
|
-
fail(`models.definitions.${name}.sandbox is no longer supported: Docker Sandboxes are no longer supported`);
|
|
206
|
-
}
|
|
207
255
|
if (Object.hasOwn(override, "disabled")) {
|
|
208
256
|
if (override["disabled"] !== true) {
|
|
209
257
|
fail(`models.definitions.${name}.disabled must be exactly \`true\` when set (got ${JSON.stringify(override["disabled"])})`);
|
|
210
258
|
}
|
|
211
|
-
const conflicting = ["cmd", "color", "usage"].filter((key) => Object.hasOwn(override, key));
|
|
259
|
+
const conflicting = ["cmd", "color", "usage", "sandbox"].filter((key) => Object.hasOwn(override, key));
|
|
212
260
|
if (conflicting.length > 0) {
|
|
213
261
|
fail(`models.definitions.${name}: cannot combine \`disabled: true\` with other fields (${conflicting.join(", ")}). Either disable the model or override its fields, not both.`);
|
|
214
262
|
}
|
|
@@ -258,7 +306,10 @@ function mergeDefinitions(user) {
|
|
|
258
306
|
if (override.usage !== undefined) {
|
|
259
307
|
candidate.usage = override.usage;
|
|
260
308
|
}
|
|
261
|
-
|
|
309
|
+
if (override.sandbox !== undefined) {
|
|
310
|
+
candidate.sandbox = normalizeSandbox(override.sandbox, `models.definitions.${name}.sandbox`);
|
|
311
|
+
}
|
|
312
|
+
const { cmd, color, usage, sandbox } = candidate;
|
|
262
313
|
if (typeof cmd !== "string" || cmd.length === 0) {
|
|
263
314
|
fail(`models.definitions.${name}.cmd must be a non-empty string`);
|
|
264
315
|
}
|
|
@@ -269,6 +320,9 @@ function mergeDefinitions(user) {
|
|
|
269
320
|
if (usage !== undefined) {
|
|
270
321
|
definition.usage = usage;
|
|
271
322
|
}
|
|
323
|
+
if (sandbox !== undefined) {
|
|
324
|
+
definition.sandbox = sandbox;
|
|
325
|
+
}
|
|
272
326
|
merged[name] = definition;
|
|
273
327
|
}
|
|
274
328
|
return merged;
|
|
@@ -294,10 +348,14 @@ function applyDefaults(user) {
|
|
|
294
348
|
requireString(user.linear.projectSlug, "linear.projectSlug");
|
|
295
349
|
requireObject(user.workspace, "workspace");
|
|
296
350
|
if (isPlainObject(user.models) && Object.hasOwn(user.models, "isolation")) {
|
|
297
|
-
fail("models.isolation is no longer supported: local
|
|
351
|
+
fail("models.isolation is no longer supported: set `local.runner` ('safehouse' | 'sdx' | 'none' | 'auto') instead");
|
|
298
352
|
}
|
|
299
353
|
if (Object.hasOwn(user, "remote")) {
|
|
300
|
-
fail("remote is no longer supported: groundcrew
|
|
354
|
+
fail("remote is no longer supported: groundcrew runs locally via safehouse/sdx/none; remove the remote block from your config");
|
|
355
|
+
}
|
|
356
|
+
const userLocal = user.local;
|
|
357
|
+
if (userLocal !== undefined && !isPlainObject(userLocal)) {
|
|
358
|
+
fail("local must be an object");
|
|
301
359
|
}
|
|
302
360
|
const slugId = extractSlugId(user.linear.projectSlug);
|
|
303
361
|
if (slugId === undefined) {
|
|
@@ -323,6 +381,9 @@ function applyDefaults(user) {
|
|
|
323
381
|
initial: user.prompts?.initial ?? DEFAULT_PROMPT_INITIAL,
|
|
324
382
|
},
|
|
325
383
|
workspaceKind: normalizeWorkspaceKind(user.workspaceKind, "workspaceKind") ?? "auto",
|
|
384
|
+
local: {
|
|
385
|
+
runner: normalizeLocalRunner(userLocal?.runner, "local.runner") ?? "auto",
|
|
386
|
+
},
|
|
326
387
|
logging: {
|
|
327
388
|
file: expandHome(normalizeOptionalString(user.logging?.file, "logging.file") ?? defaultLogFile()),
|
|
328
389
|
},
|
|
@@ -379,6 +440,13 @@ function validate(config) {
|
|
|
379
440
|
}
|
|
380
441
|
requireString(codexbar.provider, `${usagePath}.codexbar.provider`);
|
|
381
442
|
}
|
|
443
|
+
if (definition.sandbox !== undefined) {
|
|
444
|
+
requireString(definition.sandbox.agent, `models.definitions.${name}.sandbox.agent`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/* v8 ignore next 5 @preserve -- normalizeLocalRunner rejects invalid strings before validate() runs; this is a belt-and-suspenders guard */
|
|
448
|
+
if (!LOCAL_RUNNER_SETTINGS.includes(config.local.runner)) {
|
|
449
|
+
fail(`local.runner must be one of ${LOCAL_RUNNER_SETTINGS.join(", ")} (got ${JSON.stringify(config.local.runner)})`);
|
|
382
450
|
}
|
|
383
451
|
// Disabled-default check must run before the generic "not a key" check so
|
|
384
452
|
// the user gets the specific "is disabled" message instead of a stale-list
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SandboxDefinition } from "./config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Derive a deterministic sbx sandbox name from the repository + model
|
|
4
|
+
* tuple so `crew sandbox auth <repo>` and the subsequent `crew local`
|
|
5
|
+
* launch agree on which sandbox to target. Lowercased and reduced to the
|
|
6
|
+
* sbx-safe charset (`a-z0-9.+-`) so unusual repo names still round-trip
|
|
7
|
+
* cleanly. Keep the prefix stable — doctor and teardown use it to
|
|
8
|
+
* identify groundcrew-owned sandboxes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sandboxNameFor(arguments_: {
|
|
11
|
+
repository: string;
|
|
12
|
+
model: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Probe `sbx ls` to see whether a sandbox with `sandboxName` already
|
|
16
|
+
* exists. Used by `crew sandbox auth` to switch between create vs reuse
|
|
17
|
+
* branches without surfacing the raw sbx error on first run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sandboxExists(sandboxName: string, signal?: AbortSignal): Promise<boolean>;
|
|
20
|
+
interface EnsureSandboxArguments {
|
|
21
|
+
sandboxName: string;
|
|
22
|
+
sandbox: SandboxDefinition;
|
|
23
|
+
/**
|
|
24
|
+
* Host path bound into the sandbox at the same path. Pass the workspace
|
|
25
|
+
* `projectDir` so all per-ticket worktrees (siblings of the bare repo
|
|
26
|
+
* clone) are visible to `sbx exec -w <worktreeDir>` after creation.
|
|
27
|
+
*/
|
|
28
|
+
mountPath: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Idempotent guard: ensure a Docker Sandboxes container exists for the
|
|
32
|
+
* given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
|
|
33
|
+
* calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
|
|
34
|
+
* <mountPath>` to provision it. First-time agent auth still happens inside
|
|
35
|
+
* the sandbox the first time `sbx exec` runs the agent — `create` only
|
|
36
|
+
* provisions the container, it does not attach.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureSandbox(arguments_: EnsureSandboxArguments, signal?: AbortSignal): Promise<void>;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=dockerSandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMxF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/F;AAED,UAAU,sBAAsB;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { runCommandAsync } from "./commandRunner.js";
|
|
2
|
+
/**
|
|
3
|
+
* Derive a deterministic sbx sandbox name from the repository + model
|
|
4
|
+
* tuple so `crew sandbox auth <repo>` and the subsequent `crew local`
|
|
5
|
+
* launch agree on which sandbox to target. Lowercased and reduced to the
|
|
6
|
+
* sbx-safe charset (`a-z0-9.+-`) so unusual repo names still round-trip
|
|
7
|
+
* cleanly. Keep the prefix stable — doctor and teardown use it to
|
|
8
|
+
* identify groundcrew-owned sandboxes.
|
|
9
|
+
*/
|
|
10
|
+
export function sandboxNameFor(arguments_) {
|
|
11
|
+
const raw = `groundcrew-${arguments_.repository}-${arguments_.model}`.toLowerCase();
|
|
12
|
+
return raw
|
|
13
|
+
.replaceAll(/[^a-z0-9.+-]+/g, "-")
|
|
14
|
+
.replaceAll(/-+/g, "-")
|
|
15
|
+
.replaceAll(/^-|-$/g, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Probe `sbx ls` to see whether a sandbox with `sandboxName` already
|
|
19
|
+
* exists. Used by `crew sandbox auth` to switch between create vs reuse
|
|
20
|
+
* branches without surfacing the raw sbx error on first run.
|
|
21
|
+
*/
|
|
22
|
+
export async function sandboxExists(sandboxName, signal) {
|
|
23
|
+
const out = signal === undefined
|
|
24
|
+
? await runCommandAsync("sbx", ["ls"])
|
|
25
|
+
: await runCommandAsync("sbx", ["ls"], { signal });
|
|
26
|
+
return out.split("\n").some((line) => line.trim().split(/\s+/)[0] === sandboxName);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Idempotent guard: ensure a Docker Sandboxes container exists for the
|
|
30
|
+
* given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
|
|
31
|
+
* calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
|
|
32
|
+
* <mountPath>` to provision it. First-time agent auth still happens inside
|
|
33
|
+
* the sandbox the first time `sbx exec` runs the agent — `create` only
|
|
34
|
+
* provisions the container, it does not attach.
|
|
35
|
+
*/
|
|
36
|
+
export async function ensureSandbox(arguments_, signal) {
|
|
37
|
+
if (await sandboxExists(arguments_.sandboxName, signal)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const createArguments = ["create", "--name", arguments_.sandboxName];
|
|
41
|
+
if (arguments_.sandbox.template !== undefined) {
|
|
42
|
+
createArguments.push("--template", arguments_.sandbox.template);
|
|
43
|
+
}
|
|
44
|
+
for (const kit of arguments_.sandbox.kits ?? []) {
|
|
45
|
+
createArguments.push("--kit", kit);
|
|
46
|
+
}
|
|
47
|
+
createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
|
|
48
|
+
const options = signal === undefined ? {} : { signal };
|
|
49
|
+
try {
|
|
50
|
+
await runCommandAsync("sbx", createArguments, options);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (await sandboxExists(arguments_.sandboxName, signal)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/lib/host.d.ts
CHANGED
|
@@ -6,18 +6,28 @@
|
|
|
6
6
|
export interface HostCapabilities {
|
|
7
7
|
/** True when the `safehouse` binary is on PATH. */
|
|
8
8
|
hasSafehouse: boolean;
|
|
9
|
+
/** True when the `sbx` (Docker Sandboxes) binary is on PATH. */
|
|
10
|
+
hasSbx: boolean;
|
|
9
11
|
/** True when the `cmux` binary is on PATH. */
|
|
10
12
|
hasCmux: boolean;
|
|
11
13
|
/** True when the `tmux` binary is on PATH. */
|
|
12
14
|
hasTmux: boolean;
|
|
13
15
|
/** True when the host platform is macOS. Safehouse is macOS-only. */
|
|
14
16
|
isMacOS: boolean;
|
|
17
|
+
/** True when the host platform is Linux. */
|
|
18
|
+
isLinux: boolean;
|
|
15
19
|
/**
|
|
16
20
|
* True when the host platform is one Safehouse supports. Safehouse is
|
|
17
21
|
* macOS-only at time of writing; local setup uses this to reject Linux
|
|
18
22
|
* or WSL before creating a worktree.
|
|
19
23
|
*/
|
|
20
24
|
isSafehouseSupported: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* True when sdx (Docker Sandboxes) is supportable on this platform —
|
|
27
|
+
* sbx is published for both macOS and Linux, so this stays in sync with
|
|
28
|
+
* "macOS || Linux". WSL inherits Linux capabilities transparently.
|
|
29
|
+
*/
|
|
30
|
+
isSdxSupported: boolean;
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
33
|
* Resolves a binary on PATH the same way `which` does. Returns the first
|
package/dist/lib/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/lib/host.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,YAAY,EAAE,OAAO,CAAC;IACtB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/lib/host.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,YAAY,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAc1F;AAED,wBAAsB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmB5F"}
|