@clipboard-health/groundcrew 4.0.3 → 4.2.0
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 +37 -13
- package/crew.config.example.ts +5 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +27 -49
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +1 -2
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +1 -7
- package/dist/commands/upgrade.d.ts +0 -11
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +14 -100
- package/dist/lib/agentLaunch.d.ts +0 -6
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +1 -12
- package/dist/lib/cmuxAdapter.d.ts +8 -0
- package/dist/lib/cmuxAdapter.d.ts.map +1 -0
- package/dist/lib/cmuxAdapter.js +163 -0
- package/dist/lib/config.d.ts +2 -76
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +29 -102
- package/dist/lib/launchCommand.d.ts +3 -3
- package/dist/lib/sandboxName.d.ts +9 -0
- package/dist/lib/sandboxName.d.ts.map +1 -0
- package/dist/lib/sandboxName.js +12 -0
- package/dist/lib/tmuxAdapter.d.ts +9 -0
- package/dist/lib/tmuxAdapter.d.ts.map +1 -0
- package/dist/lib/tmuxAdapter.js +156 -0
- package/dist/lib/workspaceAdapter.d.ts +79 -0
- package/dist/lib/workspaceAdapter.d.ts.map +1 -0
- package/dist/lib/workspaceAdapter.js +17 -0
- package/dist/lib/workspaces.d.ts +7 -55
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +8 -404
- package/package.json +1 -2
- package/dist/commands/sandbox/auth.d.ts +0 -3
- package/dist/commands/sandbox/auth.d.ts.map +0 -1
- package/dist/commands/sandbox/auth.js +0 -227
- package/dist/commands/sandbox/index.d.ts +0 -2
- package/dist/commands/sandbox/index.d.ts.map +0 -1
- package/dist/commands/sandbox/index.js +0 -47
- package/dist/commands/sandbox/inspect.d.ts +0 -2
- package/dist/commands/sandbox/inspect.d.ts.map +0 -1
- package/dist/commands/sandbox/inspect.js +0 -18
- package/dist/commands/sandbox/lifecycle.d.ts +0 -7
- package/dist/commands/sandbox/lifecycle.d.ts.map +0 -1
- package/dist/commands/sandbox/lifecycle.js +0 -68
- package/dist/commands/sandbox/model.d.ts +0 -10
- package/dist/commands/sandbox/model.d.ts.map +0 -1
- package/dist/commands/sandbox/model.js +0 -37
- package/dist/commands/sandbox/picker.d.ts +0 -20
- package/dist/commands/sandbox/picker.d.ts.map +0 -1
- package/dist/commands/sandbox/picker.js +0 -23
- package/dist/commands/setupRepos.d.ts +0 -44
- package/dist/commands/setupRepos.d.ts.map +0 -1
- package/dist/commands/setupRepos.js +0 -212
- package/dist/lib/dockerSandbox.d.ts +0 -43
- package/dist/lib/dockerSandbox.d.ts.map +0 -1
- package/dist/lib/dockerSandbox.js +0 -69
- package/dist/lib/sandboxGitDefaults.d.ts +0 -10
- package/dist/lib/sandboxGitDefaults.d.ts.map +0 -1
- package/dist/lib/sandboxGitDefaults.js +0 -31
- package/dist/lib/upgrade.d.ts +0 -66
- package/dist/lib/upgrade.d.ts.map +0 -1
- package/dist/lib/upgrade.js +0 -178
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmux Workspace backend. cmux is the macOS TUI; workspaces surface in its
|
|
3
|
+
* own app, so `accessHint` has nothing concise to emit. cmux can paint a
|
|
4
|
+
* per-workspace status pill, which `open` applies best-effort.
|
|
5
|
+
*/
|
|
6
|
+
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
7
|
+
import { errorMessage, log } from "./util.js";
|
|
8
|
+
export const cmuxAdapter = {
|
|
9
|
+
async open(spec, signal) {
|
|
10
|
+
const output = await runWorkspaceCommand("cmux", [
|
|
11
|
+
"--json",
|
|
12
|
+
"new-workspace",
|
|
13
|
+
"--name",
|
|
14
|
+
spec.name,
|
|
15
|
+
"--cwd",
|
|
16
|
+
spec.cwd,
|
|
17
|
+
"--command",
|
|
18
|
+
spec.command,
|
|
19
|
+
], signal);
|
|
20
|
+
const workspaceId = extractCmuxOpenId(output);
|
|
21
|
+
if (workspaceId === undefined) {
|
|
22
|
+
log(`cmux new-workspace returned unrecognized output for ${spec.name}; if a workspace was created, run \`cmux close-workspace\` manually.`);
|
|
23
|
+
throw new Error(`Unexpected cmux output: ${output}`);
|
|
24
|
+
}
|
|
25
|
+
if (spec.status !== undefined) {
|
|
26
|
+
try {
|
|
27
|
+
await applyCmuxStatus(workspaceId, spec.status, signal);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Status pills are best-effort. cmux v2+ dropped `set-status` entirely,
|
|
31
|
+
// so swallow that specific gap silently; surface anything else so a real
|
|
32
|
+
// regression doesn't hide behind the same swallow.
|
|
33
|
+
if (!isCmuxSetStatusUnsupported(error)) {
|
|
34
|
+
log(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async list(signal) {
|
|
40
|
+
const raw = await listCmuxRaw(signal);
|
|
41
|
+
return raw?.map((ws) => ({ name: ws.title }));
|
|
42
|
+
},
|
|
43
|
+
async close(name, signal) {
|
|
44
|
+
const raw = await listCmuxRaw(signal);
|
|
45
|
+
if (raw === undefined) {
|
|
46
|
+
// cmux v2 `workspace.close` rejects titles, so forwarding `name`
|
|
47
|
+
// would always fail. The list failure has already been logged by
|
|
48
|
+
// `listCmuxRaw`; bail rather than guarantee a downstream error.
|
|
49
|
+
log(`cmux close-workspace skipped for ${name}: list-workspaces failed, no usable id`);
|
|
50
|
+
return { kind: "unavailable" };
|
|
51
|
+
}
|
|
52
|
+
const match = raw.find((ws) => ws.title === name);
|
|
53
|
+
if (match === undefined) {
|
|
54
|
+
return { kind: "missing" };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await closeCmuxWorkspace(match.id, signal);
|
|
58
|
+
return { kind: "closed" };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (isSignalAborted(signal)) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const remaining = await listCmuxRaw(signal);
|
|
65
|
+
if (remaining === undefined) {
|
|
66
|
+
return { kind: "unavailable", error };
|
|
67
|
+
}
|
|
68
|
+
const isStillPresent = remaining.some((ws) => ws.title === name);
|
|
69
|
+
if (!isStillPresent) {
|
|
70
|
+
return { kind: "closed" };
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
accessHint(_name) {
|
|
76
|
+
// cmux is a TUI; users surface workspaces by launching the cmux app,
|
|
77
|
+
// not a shell command. No useful hint to emit.
|
|
78
|
+
// oxlint-disable-next-line unicorn/no-useless-undefined -- explicit signal that the backend has no hint
|
|
79
|
+
return undefined;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
function parseCmuxList(output) {
|
|
83
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json list-workspaces always emits this shape
|
|
84
|
+
const parsed = JSON.parse(output);
|
|
85
|
+
const items = [];
|
|
86
|
+
/* v8 ignore next @preserve -- cmux always emits a workspaces field; default keeps the loop safe */
|
|
87
|
+
for (const ws of parsed.workspaces ?? []) {
|
|
88
|
+
if (typeof ws.title !== "string" || ws.title.length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const id = pickCmuxId(ws);
|
|
92
|
+
if (id === undefined) {
|
|
93
|
+
log(`cmux list-workspaces returned workspace "${ws.title}" without a usable id or ref; skipping`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
items.push({ title: ws.title, id });
|
|
97
|
+
}
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The stable workspace handle cmux v2 expects in JSON-RPC params. Prefer
|
|
102
|
+
* the UUID; fall back to the legacy `workspace:N` short ref when older
|
|
103
|
+
* cmux builds don't surface it. Returns `undefined` when neither is
|
|
104
|
+
* available — cmux v2 `workspace.close` rejects titles, so we must never
|
|
105
|
+
* forward `title` as a workspace handle.
|
|
106
|
+
*/
|
|
107
|
+
function pickCmuxId(ws) {
|
|
108
|
+
if (typeof ws.id === "string" && ws.id.length > 0) {
|
|
109
|
+
return ws.id;
|
|
110
|
+
}
|
|
111
|
+
if (typeof ws.ref === "string" && ws.ref.length > 0) {
|
|
112
|
+
return ws.ref;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
async function listCmuxRaw(signal) {
|
|
117
|
+
try {
|
|
118
|
+
return parseCmuxList(await runWorkspaceCommand("cmux", ["--json", "list-workspaces"], signal));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (isSignalAborted(signal)) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
log(`cmux list-workspaces failed: ${errorMessage(error)}`);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function extractCmuxOpenId(output) {
|
|
129
|
+
try {
|
|
130
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json prints a workspace_id/ref object
|
|
131
|
+
const parsed = JSON.parse(output);
|
|
132
|
+
const uuid = parsed.workspace_id ?? parsed.id ?? "";
|
|
133
|
+
if (uuid.length > 0) {
|
|
134
|
+
return uuid;
|
|
135
|
+
}
|
|
136
|
+
const ref = parsed.workspace_ref ?? parsed.ref ?? "";
|
|
137
|
+
if (ref.length > 0) {
|
|
138
|
+
return ref;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* not JSON; fall through to regex */
|
|
143
|
+
}
|
|
144
|
+
const match = /workspace:\d+/.exec(output);
|
|
145
|
+
return match ? match[0] : undefined;
|
|
146
|
+
}
|
|
147
|
+
async function applyCmuxStatus(workspaceId, status, signal) {
|
|
148
|
+
const arguments_ = ["set-status", "model", status.text];
|
|
149
|
+
if (status.icon !== undefined) {
|
|
150
|
+
arguments_.push("--icon", status.icon);
|
|
151
|
+
}
|
|
152
|
+
if (status.color !== undefined) {
|
|
153
|
+
arguments_.push("--color", status.color);
|
|
154
|
+
}
|
|
155
|
+
arguments_.push("--workspace", workspaceId);
|
|
156
|
+
await runWorkspaceCommand("cmux", arguments_, signal);
|
|
157
|
+
}
|
|
158
|
+
async function closeCmuxWorkspace(workspaceId, signal) {
|
|
159
|
+
await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
|
|
160
|
+
}
|
|
161
|
+
function isCmuxSetStatusUnsupported(error) {
|
|
162
|
+
return errorMessage(error).includes('unknown command "set-status"');
|
|
163
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -41,16 +41,12 @@ export type LocalRunnerSetting = LocalRunner | "auto";
|
|
|
41
41
|
export declare const LOCAL_RUNNER_SETTINGS: readonly LocalRunnerSetting[];
|
|
42
42
|
/**
|
|
43
43
|
* Per-model Docker Sandboxes (sdx) binding. Required at launch when
|
|
44
|
-
* `local.runner` resolves to `sdx` so groundcrew knows which
|
|
45
|
-
*
|
|
44
|
+
* `local.runner` resolves to `sdx` so groundcrew knows which existing
|
|
45
|
+
* sbx sandbox to address.
|
|
46
46
|
*/
|
|
47
47
|
export interface SandboxDefinition {
|
|
48
48
|
/** sbx agent name (e.g. "claude", "codex"). */
|
|
49
49
|
agent: string;
|
|
50
|
-
/** Optional `sbx run --template` value. */
|
|
51
|
-
template?: string;
|
|
52
|
-
/** Optional `sbx run --kit` values (each passed as a separate flag). */
|
|
53
|
-
kits?: string[];
|
|
54
50
|
/**
|
|
55
51
|
* Setup command run **inside** the sandbox before the agent exec.
|
|
56
52
|
* Defaults to the shared `.groundcrew/setup.sh --deps-only` convention
|
|
@@ -58,42 +54,6 @@ export interface SandboxDefinition {
|
|
|
58
54
|
*/
|
|
59
55
|
setupCommand?: string;
|
|
60
56
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Recipe used by `crew sandbox auth <model>` to drive an interactive
|
|
63
|
-
* login flow inside a sbx sandbox and then verify it. The flow is
|
|
64
|
-
* picker-driven — no positional `<tool>` argument; the picker lists
|
|
65
|
-
* every recipe visible to the current sandbox.
|
|
66
|
-
*
|
|
67
|
-
* `binary` defaults to the recipe key (typically the agent or CLI name).
|
|
68
|
-
* `authenticatedPattern` matches against combined stdout+stderr from
|
|
69
|
-
* `statusArgs` — exit code alone isn't reliable because some CLIs
|
|
70
|
-
* report "not logged in" while still exiting 0.
|
|
71
|
-
* `kind` controls visibility in the interactive picker: `"agent"`
|
|
72
|
-
* recipes are scoped to a specific sbx agent and only appear when you
|
|
73
|
-
* `auth` against that agent's sandbox; `"tool"` recipes (default)
|
|
74
|
-
* appear in every sandbox's picker because they're cross-cutting
|
|
75
|
-
* (github, npm, gcloud, …). Defaults to `"tool"` when omitted.
|
|
76
|
-
*
|
|
77
|
-
* Ship-side recipes for `claude`, `codex`, and `cursor` live in
|
|
78
|
-
* `src/commands/sandbox/auth.ts`; users register additional tools
|
|
79
|
-
* under `sandbox.authRecipes` in their config.
|
|
80
|
-
*/
|
|
81
|
-
export interface AuthRecipe {
|
|
82
|
-
displayName: string;
|
|
83
|
-
binary?: string;
|
|
84
|
-
loginArgs: readonly string[];
|
|
85
|
-
statusArgs: readonly string[];
|
|
86
|
-
authenticatedPattern: RegExp;
|
|
87
|
-
kind?: "agent" | "tool";
|
|
88
|
-
/**
|
|
89
|
-
* Environment variables passed to `sbx exec` for both the login and
|
|
90
|
-
* status calls. Use this for CLIs whose default flow assumes a
|
|
91
|
-
* browser or other host-only feature — e.g. cursor-agent wants
|
|
92
|
-
* `NO_OPEN_BROWSER=1` to print a device code instead of trying to
|
|
93
|
-
* launch a browser inside the sandbox.
|
|
94
|
-
*/
|
|
95
|
-
env?: Record<string, string>;
|
|
96
|
-
}
|
|
97
57
|
export interface ModelDefinition {
|
|
98
58
|
/**
|
|
99
59
|
* Shell command launched for the model. Wrapped with Safehouse/clearance
|
|
@@ -201,32 +161,6 @@ export interface Config {
|
|
|
201
161
|
local?: {
|
|
202
162
|
runner?: LocalRunnerSetting;
|
|
203
163
|
};
|
|
204
|
-
/**
|
|
205
|
-
* Sandbox-wide settings. `authRecipes` lets users register additional
|
|
206
|
-
* tools (github, npm, gcloud, …) for `crew sandbox auth <model>` to
|
|
207
|
-
* authenticate inside the sandbox. The auth flow is picker-driven —
|
|
208
|
-
* registered recipes show up in the picker alongside the shipped ones,
|
|
209
|
-
* and a user recipe under the same key (e.g. "claude") overrides the
|
|
210
|
-
* shipped one.
|
|
211
|
-
*/
|
|
212
|
-
sandbox?: {
|
|
213
|
-
authRecipes?: Record<string, AuthRecipe>;
|
|
214
|
-
/**
|
|
215
|
-
* When true (default), every `crew sandbox ensure` / `auth` run applies
|
|
216
|
-
* a small set of git defaults inside the sandbox so robot commits push
|
|
217
|
-
* over `gh`-managed HTTPS regardless of how the user cloned the repo:
|
|
218
|
-
*
|
|
219
|
-
* - disable GPG signing for commits and tags
|
|
220
|
-
* - rewrite `git@github.com:` and `ssh://git@github.com/` URLs to
|
|
221
|
-
* `https://github.com/` so push uses gh's credential helper
|
|
222
|
-
* - after a successful `github` auth recipe login, run
|
|
223
|
-
* `gh auth setup-git` inside the sandbox
|
|
224
|
-
*
|
|
225
|
-
* Set `false` to skip both the git-config block and the post-login
|
|
226
|
-
* `gh auth setup-git` step.
|
|
227
|
-
*/
|
|
228
|
-
gitDefaults?: boolean;
|
|
229
|
-
};
|
|
230
164
|
logging?: {
|
|
231
165
|
/**
|
|
232
166
|
* Append-mode log file destination. `log()` and `logEvent()` tee here
|
|
@@ -281,14 +215,6 @@ export interface ResolvedConfig {
|
|
|
281
215
|
local: {
|
|
282
216
|
runner: LocalRunnerSetting;
|
|
283
217
|
};
|
|
284
|
-
/**
|
|
285
|
-
* Sandbox-wide settings. Always present after defaults; `authRecipes`
|
|
286
|
-
* is `{}` when the user provides none.
|
|
287
|
-
*/
|
|
288
|
-
sandbox: {
|
|
289
|
-
authRecipes: Record<string, AuthRecipe>;
|
|
290
|
-
gitDefaults: boolean;
|
|
291
|
-
};
|
|
292
218
|
logging: {
|
|
293
219
|
file: string;
|
|
294
220
|
};
|
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,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAEpE;;;;;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
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -126,29 +126,6 @@ function normalizeOptionalString(value, path) {
|
|
|
126
126
|
}
|
|
127
127
|
return value.trim();
|
|
128
128
|
}
|
|
129
|
-
function normalizeOptionalBoolean(value, path) {
|
|
130
|
-
if (value === undefined) {
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
if (typeof value !== "boolean") {
|
|
134
|
-
fail(`${path} must be a boolean`);
|
|
135
|
-
}
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
function normalizeOptionalStringArray(value, path) {
|
|
139
|
-
if (value === undefined) {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
if (!Array.isArray(value)) {
|
|
143
|
-
fail(`${path} must be an array`);
|
|
144
|
-
}
|
|
145
|
-
return value.map((entry, index) => {
|
|
146
|
-
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
147
|
-
fail(`${path}[${index}] must be a non-empty string`);
|
|
148
|
-
}
|
|
149
|
-
return entry.trim();
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
129
|
function isWorkspaceKindSetting(value) {
|
|
153
130
|
return (typeof value === "string" && WORKSPACE_KIND_SETTINGS.includes(value));
|
|
154
131
|
}
|
|
@@ -177,27 +154,29 @@ function normalizeSandbox(value, path) {
|
|
|
177
154
|
if (!isPlainObject(value)) {
|
|
178
155
|
fail(`${path} must be an object`);
|
|
179
156
|
}
|
|
180
|
-
|
|
157
|
+
if (Object.hasOwn(value, "template")) {
|
|
158
|
+
failRemovedConfigKey(`${path}.template`, "Groundcrew no longer creates or re-templates sdx sandboxes.");
|
|
159
|
+
}
|
|
160
|
+
if (Object.hasOwn(value, "kits")) {
|
|
161
|
+
failRemovedConfigKey(`${path}.kits`, "Groundcrew no longer creates sdx sandboxes or applies sandbox kits.");
|
|
162
|
+
}
|
|
163
|
+
const { agent, setupCommand } = value;
|
|
181
164
|
requireString(agent, `${path}.agent`);
|
|
182
165
|
const trimmedAgent = agent.trim();
|
|
183
166
|
if (trimmedAgent.length === 0) {
|
|
184
167
|
fail(`${path}.agent must be a non-empty string (got ${JSON.stringify(agent)})`);
|
|
185
168
|
}
|
|
186
169
|
const sandbox = { agent: trimmedAgent };
|
|
187
|
-
const normalizedTemplate = normalizeOptionalString(template, `${path}.template`);
|
|
188
|
-
if (normalizedTemplate !== undefined) {
|
|
189
|
-
sandbox.template = normalizedTemplate;
|
|
190
|
-
}
|
|
191
|
-
const normalizedKits = normalizeOptionalStringArray(kits, `${path}.kits`);
|
|
192
|
-
if (normalizedKits !== undefined) {
|
|
193
|
-
sandbox.kits = normalizedKits;
|
|
194
|
-
}
|
|
195
170
|
const normalizedSetup = normalizeOptionalString(setupCommand, `${path}.setupCommand`);
|
|
196
171
|
if (normalizedSetup !== undefined) {
|
|
197
172
|
sandbox.setupCommand = normalizedSetup;
|
|
198
173
|
}
|
|
199
174
|
return sandbox;
|
|
200
175
|
}
|
|
176
|
+
function failRemovedConfigKey(path, reason) {
|
|
177
|
+
fail(`${path} is no longer supported: ${reason} ` +
|
|
178
|
+
"Provision and manage the sandbox yourself with `sbx` (for example `sbx create --name groundcrew-<agent> <agent> <projectDir>`), then keep only `models.definitions.<model>.sandbox.agent` plus optional `setupCommand` in crew.config.ts.");
|
|
179
|
+
}
|
|
201
180
|
function failIfLegacyModelKeys(name, override) {
|
|
202
181
|
if (!isPlainObject(override)) {
|
|
203
182
|
fail(`models.definitions.${name} must be an object`);
|
|
@@ -296,11 +275,6 @@ function requireObject(value, path) {
|
|
|
296
275
|
fail(`${path} must be an object (got ${JSON.stringify(value)})`);
|
|
297
276
|
}
|
|
298
277
|
}
|
|
299
|
-
function requireOptionalObject(value, path) {
|
|
300
|
-
if (value !== undefined && !isPlainObject(value)) {
|
|
301
|
-
fail(`${path} must be an object`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
278
|
function failOnLegacyLinearShape(user) {
|
|
305
279
|
if (!Object.hasOwn(user, "linear")) {
|
|
306
280
|
return;
|
|
@@ -312,6 +286,21 @@ function failOnLegacyLinearShape(user) {
|
|
|
312
286
|
"If you only want a subset of your Linear tickets to be picked up, leave the unwanted tickets unassigned or remove their `agent-*` label.",
|
|
313
287
|
].join("\n"));
|
|
314
288
|
}
|
|
289
|
+
function failOnRemovedSandboxSettings(user) {
|
|
290
|
+
const { sandbox } = user;
|
|
291
|
+
if (sandbox === undefined) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!isPlainObject(sandbox)) {
|
|
295
|
+
fail("sandbox must be an object");
|
|
296
|
+
}
|
|
297
|
+
if (Object.hasOwn(sandbox, "authRecipes")) {
|
|
298
|
+
failRemovedConfigKey("sandbox.authRecipes", "Groundcrew no longer drives in-sandbox auth flows.");
|
|
299
|
+
}
|
|
300
|
+
if (Object.hasOwn(sandbox, "gitDefaults")) {
|
|
301
|
+
failRemovedConfigKey("sandbox.gitDefaults", "Groundcrew no longer seeds git defaults inside sdx sandboxes.");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
315
304
|
function normalizeSources(raw) {
|
|
316
305
|
if (raw === undefined) {
|
|
317
306
|
return [];
|
|
@@ -348,71 +337,14 @@ function normalizeSources(raw) {
|
|
|
348
337
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- structural validation above guarantees array of {kind: string} entries; per-source Zod validation lives in buildSources
|
|
349
338
|
return raw;
|
|
350
339
|
}
|
|
351
|
-
function normalizeAuthRecipes(value, path) {
|
|
352
|
-
if (value === undefined) {
|
|
353
|
-
return {};
|
|
354
|
-
}
|
|
355
|
-
if (!isPlainObject(value)) {
|
|
356
|
-
fail(`${path} must be an object`);
|
|
357
|
-
}
|
|
358
|
-
const recipes = {};
|
|
359
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
360
|
-
const recipePath = `${path}.${key}`;
|
|
361
|
-
if (!isPlainObject(raw)) {
|
|
362
|
-
fail(`${recipePath} must be an object`);
|
|
363
|
-
}
|
|
364
|
-
const { displayName, binary, loginArgs, statusArgs, authenticatedPattern, kind, env } = raw;
|
|
365
|
-
requireString(displayName, `${recipePath}.displayName`);
|
|
366
|
-
const loginArray = normalizeOptionalStringArray(loginArgs, `${recipePath}.loginArgs`);
|
|
367
|
-
const statusArray = normalizeOptionalStringArray(statusArgs, `${recipePath}.statusArgs`);
|
|
368
|
-
if (loginArray === undefined) {
|
|
369
|
-
fail(`${recipePath}.loginArgs is required`);
|
|
370
|
-
}
|
|
371
|
-
if (statusArray === undefined) {
|
|
372
|
-
fail(`${recipePath}.statusArgs is required`);
|
|
373
|
-
}
|
|
374
|
-
if (!(authenticatedPattern instanceof RegExp)) {
|
|
375
|
-
fail(`${recipePath}.authenticatedPattern must be a RegExp`);
|
|
376
|
-
}
|
|
377
|
-
const recipe = {
|
|
378
|
-
displayName,
|
|
379
|
-
loginArgs: loginArray,
|
|
380
|
-
statusArgs: statusArray,
|
|
381
|
-
authenticatedPattern,
|
|
382
|
-
};
|
|
383
|
-
const binaryString = normalizeOptionalString(binary, `${recipePath}.binary`);
|
|
384
|
-
if (binaryString !== undefined) {
|
|
385
|
-
recipe.binary = binaryString;
|
|
386
|
-
}
|
|
387
|
-
if (kind !== undefined) {
|
|
388
|
-
if (kind !== "agent" && kind !== "tool") {
|
|
389
|
-
fail(`${recipePath}.kind must be "agent" or "tool"`);
|
|
390
|
-
}
|
|
391
|
-
recipe.kind = kind;
|
|
392
|
-
}
|
|
393
|
-
if (env !== undefined) {
|
|
394
|
-
if (!isPlainObject(env)) {
|
|
395
|
-
fail(`${recipePath}.env must be an object`);
|
|
396
|
-
}
|
|
397
|
-
const normalizedEnv = {};
|
|
398
|
-
for (const [envKey, envValue] of Object.entries(env)) {
|
|
399
|
-
if (typeof envValue !== "string") {
|
|
400
|
-
fail(`${recipePath}.env.${envKey} must be a string`);
|
|
401
|
-
}
|
|
402
|
-
normalizedEnv[envKey] = envValue;
|
|
403
|
-
}
|
|
404
|
-
recipe.env = normalizedEnv;
|
|
405
|
-
}
|
|
406
|
-
recipes[key] = recipe;
|
|
407
|
-
}
|
|
408
|
-
return recipes;
|
|
409
|
-
}
|
|
410
340
|
function applyDefaults(user) {
|
|
411
341
|
// Guard the top-level shape before reading nested fields, so a
|
|
412
342
|
// malformed runtime config produces a `groundcrew config: ...` error
|
|
413
343
|
// instead of a raw `TypeError: Cannot read properties of undefined`.
|
|
414
344
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- `user` is loosely typed input from the loader; we narrow with requireObject below
|
|
415
|
-
|
|
345
|
+
const rawUser = user;
|
|
346
|
+
failOnLegacyLinearShape(rawUser);
|
|
347
|
+
failOnRemovedSandboxSettings(rawUser);
|
|
416
348
|
requireObject(user.workspace, "workspace");
|
|
417
349
|
if (isPlainObject(user.models) && Object.hasOwn(user.models, "isolation")) {
|
|
418
350
|
fail("models.isolation is no longer supported: set `local.runner` ('safehouse' | 'sdx' | 'none' | 'auto') instead");
|
|
@@ -420,7 +352,6 @@ function applyDefaults(user) {
|
|
|
420
352
|
if (Object.hasOwn(user, "remote")) {
|
|
421
353
|
fail("remote is no longer supported: groundcrew runs locally via safehouse/sdx/none; remove the remote block from your config");
|
|
422
354
|
}
|
|
423
|
-
requireOptionalObject(user.sandbox, "sandbox");
|
|
424
355
|
const userLocal = user.local;
|
|
425
356
|
if (userLocal !== undefined && !isPlainObject(userLocal)) {
|
|
426
357
|
fail("local must be an object");
|
|
@@ -445,10 +376,6 @@ function applyDefaults(user) {
|
|
|
445
376
|
local: {
|
|
446
377
|
runner: normalizeLocalRunner(userLocal?.runner, "local.runner") ?? "auto",
|
|
447
378
|
},
|
|
448
|
-
sandbox: {
|
|
449
|
-
authRecipes: normalizeAuthRecipes(user.sandbox?.authRecipes, "sandbox.authRecipes"),
|
|
450
|
-
gitDefaults: normalizeOptionalBoolean(user.sandbox?.gitDefaults, "sandbox.gitDefaults") ?? true,
|
|
451
|
-
},
|
|
452
379
|
logging: {
|
|
453
380
|
file: expandHome(normalizeOptionalString(user.logging?.file, "logging.file") ?? defaultLogFile()),
|
|
454
381
|
},
|
|
@@ -36,9 +36,9 @@ interface LaunchCommandArguments {
|
|
|
36
36
|
runner: LocalRunner;
|
|
37
37
|
/**
|
|
38
38
|
* sbx sandbox name when `runner === "sdx"`. Derived by the caller from
|
|
39
|
-
* `sandboxNameFor({
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* `sandboxNameFor({ agent })`. Required for sdx; ignored otherwise.
|
|
40
|
+
* Kept off the model definition so a model can launch under safehouse
|
|
41
|
+
* on one host and sdx on another without config edits.
|
|
42
42
|
*/
|
|
43
43
|
sandboxName?: string | undefined;
|
|
44
44
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the sbx sandbox name groundcrew expects for a given sbx agent.
|
|
3
|
+
* Groundcrew only addresses this existing sandbox at launch time; it does
|
|
4
|
+
* not probe, create, mutate, or remove it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sandboxNameFor(arguments_: {
|
|
7
|
+
agent: string;
|
|
8
|
+
}): string;
|
|
9
|
+
//# sourceMappingURL=sandboxName.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandboxName.d.ts","sourceRoot":"","sources":["../../src/lib/sandboxName.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the sbx sandbox name groundcrew expects for a given sbx agent.
|
|
3
|
+
* Groundcrew only addresses this existing sandbox at launch time; it does
|
|
4
|
+
* not probe, create, mutate, or remove it.
|
|
5
|
+
*/
|
|
6
|
+
export function sandboxNameFor(arguments_) {
|
|
7
|
+
const raw = `groundcrew-${arguments_.agent}`.toLowerCase();
|
|
8
|
+
return raw
|
|
9
|
+
.replaceAll(/[^a-z0-9.+-]+/g, "-")
|
|
10
|
+
.replaceAll(/-+/g, "-")
|
|
11
|
+
.replaceAll(/^-|-$/g, "");
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux Workspace backend. Workspaces live as windows inside one dedicated
|
|
3
|
+
* `groundcrew` tmux session; the window name is the ticket id. tmux can't
|
|
4
|
+
* paint status pills, so `open` silently drops `spec.status`. This is the
|
|
5
|
+
* Linux/WSL path where cmux is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
import { type Adapter } from "./workspaceAdapter.ts";
|
|
8
|
+
export declare const tmuxAdapter: Adapter;
|
|
9
|
+
//# sourceMappingURL=tmuxAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,OAgEzB,CAAC"}
|