@clipboard-health/groundcrew 4.0.2 → 4.1.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 +32 -13
- package/crew.config.example.ts +5 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +64 -10
- package/dist/commands/interruptWorkspace.d.ts.map +1 -1
- package/dist/commands/interruptWorkspace.js +3 -3
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +1 -2
- package/dist/commands/setupRepos.d.ts.map +1 -1
- package/dist/commands/setupRepos.js +2 -13
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +1 -7
- 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/util.d.ts +11 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +21 -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/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
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { runCommandAsync } from "../../lib/commandRunner.js";
|
|
2
|
-
import { writeOutput } from "../../lib/util.js";
|
|
3
|
-
import { ensureOne } from "./lifecycle.js";
|
|
4
|
-
import { resolveModel, sandboxModels } from "./model.js";
|
|
5
|
-
import { pickTools } from "./picker.js";
|
|
6
|
-
/**
|
|
7
|
-
* Built-in recipes shipped with crew. Users register additional tools
|
|
8
|
-
* by adding entries under `sandbox.authRecipes` in `crew.config.ts`;
|
|
9
|
-
* a user recipe under the same key overrides the built-in.
|
|
10
|
-
*
|
|
11
|
-
* `kind: "agent"` recipes only appear in the picker when the current
|
|
12
|
-
* sandbox's agent matches the recipe key. `kind: "tool"` (the default
|
|
13
|
-
* for user recipes) is cross-cutting and always appears.
|
|
14
|
-
*/
|
|
15
|
-
const BUILTIN_AUTH_RECIPES = {
|
|
16
|
-
claude: {
|
|
17
|
-
displayName: "Claude",
|
|
18
|
-
loginArgs: ["auth", "login"],
|
|
19
|
-
statusArgs: ["auth", "status"],
|
|
20
|
-
authenticatedPattern: /"loggedIn"\s*:\s*true/,
|
|
21
|
-
kind: "agent",
|
|
22
|
-
},
|
|
23
|
-
codex: {
|
|
24
|
-
displayName: "Codex",
|
|
25
|
-
// `--device-auth` keeps the OAuth flow headless: codex prints a URL
|
|
26
|
-
// and a code instead of trying to open a browser inside the sandbox.
|
|
27
|
-
loginArgs: ["login", "--device-auth"],
|
|
28
|
-
statusArgs: ["login", "status"],
|
|
29
|
-
// Match "Logged in using …" but not a hypothetical "Not logged in".
|
|
30
|
-
authenticatedPattern: /(^|\W)Logged in using\b/i,
|
|
31
|
-
kind: "agent",
|
|
32
|
-
},
|
|
33
|
-
cursor: {
|
|
34
|
-
displayName: "Cursor",
|
|
35
|
-
binary: "cursor-agent",
|
|
36
|
-
loginArgs: ["login"],
|
|
37
|
-
statusArgs: ["status"],
|
|
38
|
-
// Authenticated output is "✓ Logged in as <email>"; the unauthenticated
|
|
39
|
-
// output is "Not logged in", which a loose /Logged in/i would falsely
|
|
40
|
-
// match.
|
|
41
|
-
authenticatedPattern: /Logged in as\b/i,
|
|
42
|
-
kind: "agent",
|
|
43
|
-
// cursor-agent tries to open a browser by default and silently
|
|
44
|
-
// writes a partial auth file when xdg-open fails; this env var
|
|
45
|
-
// switches it to a device-code flow that works without a browser.
|
|
46
|
-
env: { NO_OPEN_BROWSER: "1" },
|
|
47
|
-
},
|
|
48
|
-
github: {
|
|
49
|
-
displayName: "GitHub CLI",
|
|
50
|
-
binary: "gh",
|
|
51
|
-
loginArgs: ["auth", "login"],
|
|
52
|
-
statusArgs: ["auth", "status"],
|
|
53
|
-
authenticatedPattern: /Logged in to github\.com/i,
|
|
54
|
-
kind: "tool",
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
function binaryFor(toolKey, recipe) {
|
|
58
|
-
return recipe.binary ?? toolKey;
|
|
59
|
-
}
|
|
60
|
-
function envFlags(recipe) {
|
|
61
|
-
const entries = Object.entries(recipe.env ?? {});
|
|
62
|
-
return entries.flatMap(([key, value]) => ["-e", `${key}=${value}`]);
|
|
63
|
-
}
|
|
64
|
-
// User-supplied recipes can carry arbitrary tokens; wrap each in single
|
|
65
|
-
// quotes so spaces and shell metacharacters can't change how the in-sandbox
|
|
66
|
-
// shell tokenizes the status command.
|
|
67
|
-
function shellQuote(value) {
|
|
68
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
69
|
-
}
|
|
70
|
-
async function probeAuthStatus(sandboxName, toolKey, recipe) {
|
|
71
|
-
// Some CLIs print status to stderr instead of stdout (codex does
|
|
72
|
-
// this). Fold stderr into stdout via the in-sandbox shell so the
|
|
73
|
-
// pattern match sees the message regardless of which stream it
|
|
74
|
-
// landed on.
|
|
75
|
-
const innerCommand = `${[binaryFor(toolKey, recipe), ...recipe.statusArgs]
|
|
76
|
-
.map(shellQuote)
|
|
77
|
-
.join(" ")} 2>&1`;
|
|
78
|
-
try {
|
|
79
|
-
const output = await runCommandAsync("sbx", [
|
|
80
|
-
"exec",
|
|
81
|
-
...envFlags(recipe),
|
|
82
|
-
sandboxName,
|
|
83
|
-
"sh",
|
|
84
|
-
"-c",
|
|
85
|
-
innerCommand,
|
|
86
|
-
]);
|
|
87
|
-
// Reset lastIndex so a /g or /y user recipe doesn't carry state
|
|
88
|
-
// across probes and return a false negative.
|
|
89
|
-
recipe.authenticatedPattern.lastIndex = 0;
|
|
90
|
-
return recipe.authenticatedPattern.test(output);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function loginAndVerify(input) {
|
|
97
|
-
const { sandboxName, toolKey, recipe, modelName, gitDefaults } = input;
|
|
98
|
-
const binary = binaryFor(toolKey, recipe);
|
|
99
|
-
writeOutput(`${sandboxName}: launching '${recipe.displayName}' login...`);
|
|
100
|
-
writeOutput("Complete the login flow in the prompts/browser, then return here.");
|
|
101
|
-
await runCommandAsync("sbx", ["exec", "-it", ...envFlags(recipe), sandboxName, binary, ...recipe.loginArgs], { stdio: "inherit" });
|
|
102
|
-
writeOutput("");
|
|
103
|
-
writeOutput(`${sandboxName}: verifying '${recipe.displayName}' authentication...`);
|
|
104
|
-
const authenticated = await probeAuthStatus(sandboxName, toolKey, recipe);
|
|
105
|
-
if (authenticated) {
|
|
106
|
-
writeOutput(`${sandboxName}: '${recipe.displayName}' authenticated.`);
|
|
107
|
-
if (gitDefaults && toolKey === "github" && binary === "gh") {
|
|
108
|
-
await runGhSetupGit(sandboxName);
|
|
109
|
-
}
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
writeOutput(`${sandboxName}: could not confirm '${recipe.displayName}' authentication — re-run 'crew sandbox auth ${modelName}' to retry.`);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Register `gh` as git's credential helper inside the sandbox so HTTPS
|
|
116
|
-
* pushes succeed without prompting. Best-effort — a failure here doesn't
|
|
117
|
-
* undo the login itself, so we warn and move on.
|
|
118
|
-
*/
|
|
119
|
-
async function runGhSetupGit(sandboxName) {
|
|
120
|
-
writeOutput(`${sandboxName}: wiring 'gh' as git credential helper...`);
|
|
121
|
-
try {
|
|
122
|
-
await runCommandAsync("sbx", ["exec", sandboxName, "gh", "auth", "setup-git"]);
|
|
123
|
-
writeOutput(`${sandboxName}: 'gh auth setup-git' done.`);
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
writeOutput(`${sandboxName}: warning — 'gh auth setup-git' failed: ${String(error)}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function availableRecipes(config) {
|
|
130
|
-
const merged = {
|
|
131
|
-
...BUILTIN_AUTH_RECIPES,
|
|
132
|
-
...config.sandbox.authRecipes,
|
|
133
|
-
};
|
|
134
|
-
return Object.entries(merged)
|
|
135
|
-
.map(([key, recipe]) => ({ key, recipe }))
|
|
136
|
-
.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
137
|
-
}
|
|
138
|
-
function shouldShowInPicker(entry, currentAgent) {
|
|
139
|
-
// Tools (the default) appear in every sandbox. Agent recipes only
|
|
140
|
-
// appear when they match the current sandbox's agent, so opening
|
|
141
|
-
// 'crew sandbox auth codex' doesn't list Claude or Cursor.
|
|
142
|
-
const kind = entry.recipe.kind ?? "tool";
|
|
143
|
-
return kind === "tool" || entry.key === currentAgent;
|
|
144
|
-
}
|
|
145
|
-
const AUTH_USAGE = "Usage: crew sandbox auth <model> | --all";
|
|
146
|
-
function parseAuthArgs(config, argv) {
|
|
147
|
-
const positionals = [];
|
|
148
|
-
let all = false;
|
|
149
|
-
for (const argument of argv) {
|
|
150
|
-
if (argument === "--all") {
|
|
151
|
-
all = true;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (argument.startsWith("-")) {
|
|
155
|
-
throw new Error(`crew sandbox auth: unknown option '${argument}'`);
|
|
156
|
-
}
|
|
157
|
-
positionals.push(argument);
|
|
158
|
-
}
|
|
159
|
-
if (all && positionals.length > 0) {
|
|
160
|
-
throw new Error("crew sandbox auth: --all cannot be combined with a model name");
|
|
161
|
-
}
|
|
162
|
-
if (all) {
|
|
163
|
-
const models = sandboxModels(config);
|
|
164
|
-
if (models.length === 0) {
|
|
165
|
-
throw new Error("crew sandbox auth --all: no sandbox-bearing models configured");
|
|
166
|
-
}
|
|
167
|
-
return { models: models.map((model) => ({ modelName: model.modelName, model })) };
|
|
168
|
-
}
|
|
169
|
-
const [modelName, ...extras] = positionals;
|
|
170
|
-
if (modelName === undefined || extras.length > 0) {
|
|
171
|
-
throw new Error(AUTH_USAGE);
|
|
172
|
-
}
|
|
173
|
-
return { models: [{ modelName, model: resolveModel(config, modelName) }] };
|
|
174
|
-
}
|
|
175
|
-
export async function runAuth(config, argv) {
|
|
176
|
-
const { models } = parseAuthArgs(config, argv);
|
|
177
|
-
for (const [index, { modelName, model }] of models.entries()) {
|
|
178
|
-
if (models.length > 1) {
|
|
179
|
-
writeOutput("");
|
|
180
|
-
writeOutput(`════ ${modelName} (${index + 1}/${models.length}) ════`);
|
|
181
|
-
}
|
|
182
|
-
writeOutput(`${model.sandboxName}: ensuring sandbox is up...`);
|
|
183
|
-
// oxlint-disable-next-line no-await-in-loop -- each sandbox is interactive; running them sequentially keeps the prompts coherent
|
|
184
|
-
await ensureOne(config, model);
|
|
185
|
-
writeOutput("");
|
|
186
|
-
// oxlint-disable-next-line no-await-in-loop -- intentionally sequential, see above
|
|
187
|
-
await runAuthInteractive(config, model, modelName);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
async function runAuthInteractive(config, model, modelName) {
|
|
191
|
-
const recipes = availableRecipes(config).filter((entry) => shouldShowInPicker(entry, model.sandbox.agent));
|
|
192
|
-
writeOutput(`${model.sandboxName}: probing authentication status for ${recipes.length} tools...`);
|
|
193
|
-
const statuses = await Promise.all(recipes.map(async ({ key, recipe }) => ({
|
|
194
|
-
key,
|
|
195
|
-
recipe,
|
|
196
|
-
authenticated: await probeAuthStatus(model.sandboxName, key, recipe),
|
|
197
|
-
})));
|
|
198
|
-
const choices = statuses.map(({ key, recipe, authenticated }) => ({
|
|
199
|
-
key,
|
|
200
|
-
label: `${recipe.displayName} (${key})`,
|
|
201
|
-
authenticated,
|
|
202
|
-
}));
|
|
203
|
-
writeOutput("");
|
|
204
|
-
const selectedKeys = await pickTools(choices);
|
|
205
|
-
if (selectedKeys.length === 0) {
|
|
206
|
-
writeOutput("Nothing selected. Exiting.");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const selectedRecipes = new Map(statuses.map((entry) => [entry.key, entry.recipe]));
|
|
210
|
-
for (const key of selectedKeys) {
|
|
211
|
-
const recipe = selectedRecipes.get(key);
|
|
212
|
-
/* v8 ignore next 3 @preserve - defensive; selectedKeys come from the same map */
|
|
213
|
-
if (recipe === undefined) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
writeOutput("");
|
|
217
|
-
writeOutput(`── ${recipe.displayName} ──`);
|
|
218
|
-
// oxlint-disable-next-line no-await-in-loop -- each login is interactive; running them sequentially keeps the prompts coherent
|
|
219
|
-
await loginAndVerify({
|
|
220
|
-
sandboxName: model.sandboxName,
|
|
221
|
-
toolKey: key,
|
|
222
|
-
recipe,
|
|
223
|
-
modelName,
|
|
224
|
-
gitDefaults: config.sandbox.gitDefaults,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/index.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B9D"}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { loadConfig } from "../../lib/config.js";
|
|
2
|
-
import { runAuth } from "./auth.js";
|
|
3
|
-
import { runList } from "./inspect.js";
|
|
4
|
-
import { runEnsure, runRegenerate, runRemove } from "./lifecycle.js";
|
|
5
|
-
const USAGE = [
|
|
6
|
-
"Usage: crew sandbox <verb> [...args]",
|
|
7
|
-
"",
|
|
8
|
-
"Verbs:",
|
|
9
|
-
" list Show every groundcrew-owned sandbox known to sbx",
|
|
10
|
-
" ensure [<model>] Provision the sandbox for one model, or all when omitted",
|
|
11
|
-
" regenerate <model>|--all Tear down and recreate from current template/kits",
|
|
12
|
-
" auth <model>|--all Open a checkbox picker of every tool available in <model>'s",
|
|
13
|
-
" sandbox and run the login flow for each one you select;",
|
|
14
|
-
" --all loops through every configured sandbox in turn",
|
|
15
|
-
" rm <model> Remove the sandbox for a model",
|
|
16
|
-
].join("\n");
|
|
17
|
-
export async function sandboxCli(argv) {
|
|
18
|
-
const [verb, ...rest] = argv;
|
|
19
|
-
if (verb === undefined) {
|
|
20
|
-
throw new Error(USAGE);
|
|
21
|
-
}
|
|
22
|
-
switch (verb) {
|
|
23
|
-
case "list": {
|
|
24
|
-
await runList();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
case "ensure": {
|
|
28
|
-
await runEnsure(await loadConfig(), rest);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
case "regenerate": {
|
|
32
|
-
await runRegenerate(await loadConfig(), rest);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
case "auth": {
|
|
36
|
-
await runAuth(await loadConfig(), rest);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
case "rm": {
|
|
40
|
-
await runRemove(await loadConfig(), rest);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
default: {
|
|
44
|
-
throw new Error(`Unknown sandbox sub-verb: ${verb}\n${USAGE}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/inspect.ts"],"names":[],"mappings":"AAKA,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAc7C"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { runCommandAsync } from "../../lib/commandRunner.js";
|
|
2
|
-
import { writeOutput } from "../../lib/util.js";
|
|
3
|
-
const SANDBOX_NAME_PREFIX = "groundcrew-";
|
|
4
|
-
export async function runList() {
|
|
5
|
-
const output = await runCommandAsync("sbx", ["ls"]);
|
|
6
|
-
const names = output
|
|
7
|
-
.split("\n")
|
|
8
|
-
.map((line) => line.trim().split(/\s+/)[0])
|
|
9
|
-
.filter((name) => name !== undefined && name.startsWith(SANDBOX_NAME_PREFIX))
|
|
10
|
-
.map((name) => name.slice(SANDBOX_NAME_PREFIX.length));
|
|
11
|
-
if (names.length === 0) {
|
|
12
|
-
writeOutput("(none)");
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
for (const name of names) {
|
|
16
|
-
writeOutput(name);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { ResolvedConfig } from "../../lib/config.ts";
|
|
2
|
-
import { type SandboxModel } from "./model.ts";
|
|
3
|
-
export declare function ensureOne(config: ResolvedConfig, model: SandboxModel, alreadyExists?: boolean): Promise<void>;
|
|
4
|
-
export declare function runEnsure(config: ResolvedConfig, argv: string[]): Promise<void>;
|
|
5
|
-
export declare function runRegenerate(config: ResolvedConfig, argv: string[]): Promise<void>;
|
|
6
|
-
export declare function runRemove(config: ResolvedConfig, argv: string[]): Promise<void>;
|
|
7
|
-
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/lifecycle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAsC,KAAK,YAAY,EAAiB,MAAM,YAAY,CAAC;AAElG,wBAAsB,SAAS,CAC7B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD,wBAAsB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrF;AAUD,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrF"}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { runCommandAsync } from "../../lib/commandRunner.js";
|
|
3
|
-
import { ensureSandbox, sandboxExists } from "../../lib/dockerSandbox.js";
|
|
4
|
-
import { writeOutput } from "../../lib/util.js";
|
|
5
|
-
import { requireOnePositional, resolveModel, sandboxModels } from "./model.js";
|
|
6
|
-
export async function ensureOne(config, model, alreadyExists) {
|
|
7
|
-
await ensureSandbox({
|
|
8
|
-
sandboxName: model.sandboxName,
|
|
9
|
-
sandbox: model.sandbox,
|
|
10
|
-
mountPath: resolve(config.workspace.projectDir),
|
|
11
|
-
gitDefaults: config.sandbox.gitDefaults,
|
|
12
|
-
...(alreadyExists === undefined ? {} : { alreadyExists }),
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
async function removeOne(model) {
|
|
16
|
-
await runCommandAsync("sbx", ["rm", "--force", model.sandboxName]);
|
|
17
|
-
}
|
|
18
|
-
export async function runEnsure(config, argv) {
|
|
19
|
-
const targets = argv.length === 0
|
|
20
|
-
? sandboxModels(config)
|
|
21
|
-
: [resolveModel(config, requireOnePositional(argv, "Usage: crew sandbox ensure [<model>]"))];
|
|
22
|
-
if (targets.length === 0) {
|
|
23
|
-
writeOutput("No sandbox models configured.");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
for (const model of targets) {
|
|
27
|
-
// oxlint-disable-next-line no-await-in-loop -- one sandbox at a time; probe then ensure
|
|
28
|
-
const existed = await sandboxExists(model.sandboxName);
|
|
29
|
-
writeOutput(existed
|
|
30
|
-
? `${model.sandboxName}: already exists`
|
|
31
|
-
: `${model.sandboxName}: creating (agent=${model.sandbox.agent}, template=${model.sandbox.template ?? "default"})`);
|
|
32
|
-
// oxlint-disable-next-line no-await-in-loop -- sbx create is intentionally sequential
|
|
33
|
-
await ensureOne(config, model, existed);
|
|
34
|
-
if (!existed) {
|
|
35
|
-
writeOutput(`${model.sandboxName}: created`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function regenerateTargets(config, argv) {
|
|
40
|
-
const target = requireOnePositional(argv, "Usage: crew sandbox regenerate <model>|--all");
|
|
41
|
-
if (target === "--all") {
|
|
42
|
-
return sandboxModels(config);
|
|
43
|
-
}
|
|
44
|
-
return [resolveModel(config, target)];
|
|
45
|
-
}
|
|
46
|
-
export async function runRegenerate(config, argv) {
|
|
47
|
-
const targets = regenerateTargets(config, argv);
|
|
48
|
-
if (targets.length === 0) {
|
|
49
|
-
writeOutput("No sandbox models configured.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
for (const model of targets) {
|
|
53
|
-
writeOutput(`${model.sandboxName}: removing existing sandbox...`);
|
|
54
|
-
// oxlint-disable-next-line no-await-in-loop -- sbx rm/create are intentionally sequential
|
|
55
|
-
await removeOne(model);
|
|
56
|
-
writeOutput(`${model.sandboxName}: creating (agent=${model.sandbox.agent}, template=${model.sandbox.template ?? "default"})`);
|
|
57
|
-
// oxlint-disable-next-line no-await-in-loop -- sbx rm/create are intentionally sequential
|
|
58
|
-
await ensureOne(config, model, false);
|
|
59
|
-
writeOutput(`${model.sandboxName}: regenerated`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export async function runRemove(config, argv) {
|
|
63
|
-
const modelName = requireOnePositional(argv, "Usage: crew sandbox rm <model>");
|
|
64
|
-
const model = resolveModel(config, modelName);
|
|
65
|
-
writeOutput(`${model.sandboxName}: removing...`);
|
|
66
|
-
await removeOne(model);
|
|
67
|
-
writeOutput(`${model.sandboxName}: removed`);
|
|
68
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { ResolvedConfig, SandboxDefinition } from "../../lib/config.ts";
|
|
2
|
-
export interface SandboxModel {
|
|
3
|
-
modelName: string;
|
|
4
|
-
sandbox: SandboxDefinition;
|
|
5
|
-
sandboxName: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function sandboxModels(config: ResolvedConfig): SandboxModel[];
|
|
8
|
-
export declare function resolveModel(config: ResolvedConfig, modelName: string): SandboxModel;
|
|
9
|
-
export declare function requireOnePositional(argv: string[], usage: string): string;
|
|
10
|
-
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7E,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAcpE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAapF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1E"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { sandboxNameFor } from "../../lib/dockerSandbox.js";
|
|
2
|
-
export function sandboxModels(config) {
|
|
3
|
-
const models = [];
|
|
4
|
-
for (const [modelName, definition] of Object.entries(config.models.definitions)) {
|
|
5
|
-
const { sandbox } = definition;
|
|
6
|
-
if (sandbox === undefined) {
|
|
7
|
-
continue;
|
|
8
|
-
}
|
|
9
|
-
models.push({
|
|
10
|
-
modelName,
|
|
11
|
-
sandbox,
|
|
12
|
-
sandboxName: sandboxNameFor({ agent: sandbox.agent }),
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
return models;
|
|
16
|
-
}
|
|
17
|
-
export function resolveModel(config, modelName) {
|
|
18
|
-
const definition = config.models.definitions[modelName];
|
|
19
|
-
if (definition === undefined) {
|
|
20
|
-
throw new Error(`crew sandbox: unknown model '${modelName}'`);
|
|
21
|
-
}
|
|
22
|
-
if (definition.sandbox === undefined) {
|
|
23
|
-
throw new Error(`crew sandbox: model '${modelName}' has no sandbox config`);
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
modelName,
|
|
27
|
-
sandbox: definition.sandbox,
|
|
28
|
-
sandboxName: sandboxNameFor({ agent: definition.sandbox.agent }),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export function requireOnePositional(argv, usage) {
|
|
32
|
-
const [first, ...rest] = argv;
|
|
33
|
-
if (first === undefined || rest.length > 0) {
|
|
34
|
-
throw new Error(usage);
|
|
35
|
-
}
|
|
36
|
-
return first;
|
|
37
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface ToolChoice {
|
|
2
|
-
/** Recipe key (e.g. "claude", "github"). Returned in the selection. */
|
|
3
|
-
key: string;
|
|
4
|
-
/** Human-friendly label shown in the prompt. */
|
|
5
|
-
label: string;
|
|
6
|
-
/** Auth status decoration: ✓ when authenticated, ○ otherwise. */
|
|
7
|
-
authenticated: boolean;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Show an interactive checkbox picker so the engineer chooses which
|
|
11
|
-
* tools to authenticate. Items marked `authenticated` start unchecked
|
|
12
|
-
* (no need to re-auth); unauthed items start checked (default action
|
|
13
|
-
* is "auth what's missing"). The returned array is the list of `key`
|
|
14
|
-
* values that the engineer left checked when they confirmed.
|
|
15
|
-
*
|
|
16
|
-
* Extracted to its own module so tests can vi.mock it and skip stdin
|
|
17
|
-
* interaction; the real implementation pulls @inquirer/checkbox.
|
|
18
|
-
*/
|
|
19
|
-
export declare function pickTools(choices: readonly ToolChoice[]): Promise<readonly string[]>;
|
|
20
|
-
//# sourceMappingURL=picker.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../../src/commands/sandbox/picker.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAW1F"}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import checkbox from "@inquirer/checkbox";
|
|
2
|
-
/**
|
|
3
|
-
* Show an interactive checkbox picker so the engineer chooses which
|
|
4
|
-
* tools to authenticate. Items marked `authenticated` start unchecked
|
|
5
|
-
* (no need to re-auth); unauthed items start checked (default action
|
|
6
|
-
* is "auth what's missing"). The returned array is the list of `key`
|
|
7
|
-
* values that the engineer left checked when they confirmed.
|
|
8
|
-
*
|
|
9
|
-
* Extracted to its own module so tests can vi.mock it and skip stdin
|
|
10
|
-
* interaction; the real implementation pulls @inquirer/checkbox.
|
|
11
|
-
*/
|
|
12
|
-
export async function pickTools(choices) {
|
|
13
|
-
const selected = await checkbox({
|
|
14
|
-
message: "Select tools to authenticate (space to toggle, enter to confirm):",
|
|
15
|
-
choices: choices.map((choice) => ({
|
|
16
|
-
name: `${choice.authenticated ? "✓" : "○"} ${choice.label}`,
|
|
17
|
-
value: choice.key,
|
|
18
|
-
checked: !choice.authenticated,
|
|
19
|
-
})),
|
|
20
|
-
pageSize: Math.max(choices.length, 1),
|
|
21
|
-
});
|
|
22
|
-
return selected;
|
|
23
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { SandboxDefinition } from "./config.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Derive a deterministic sbx sandbox name from the sbx agent so every
|
|
4
|
-
* groundcrew model that targets the same agent reuses one sandbox across
|
|
5
|
-
* repositories and tickets. Lowercased and reduced to the sbx-safe
|
|
6
|
-
* charset (`a-z0-9.+-`) so unusual agent names still round-trip cleanly.
|
|
7
|
-
* Keep the `groundcrew-` prefix stable — doctor and teardown use it to
|
|
8
|
-
* identify groundcrew-owned sandboxes.
|
|
9
|
-
*/
|
|
10
|
-
export declare function sandboxNameFor(arguments_: {
|
|
11
|
-
agent: string;
|
|
12
|
-
}): string;
|
|
13
|
-
/**
|
|
14
|
-
* Probe `sbx ls` to see whether a sandbox with `sandboxName` already
|
|
15
|
-
* exists. Used by `crew sandbox auth` to switch between create vs reuse
|
|
16
|
-
* branches without surfacing the raw sbx error on first run.
|
|
17
|
-
*/
|
|
18
|
-
export declare function sandboxExists(sandboxName: string, signal?: AbortSignal): Promise<boolean>;
|
|
19
|
-
interface EnsureSandboxArguments {
|
|
20
|
-
sandboxName: string;
|
|
21
|
-
sandbox: SandboxDefinition;
|
|
22
|
-
/**
|
|
23
|
-
* Host path bound into the sandbox at the same path. Pass the workspace
|
|
24
|
-
* `projectDir` so all per-ticket worktrees (siblings of the bare repo
|
|
25
|
-
* clone) are visible to `sbx exec -w <worktreeDir>` after creation.
|
|
26
|
-
*/
|
|
27
|
-
mountPath: string;
|
|
28
|
-
/**
|
|
29
|
-
* When true, apply the standard git defaults inside the sandbox after
|
|
30
|
-
* it exists (idempotent, runs whether the sandbox was just created or
|
|
31
|
-
* already there). See `sandboxGitDefaults.ts` for what gets set.
|
|
32
|
-
*/
|
|
33
|
-
gitDefaults: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Result of an earlier `sandboxExists` probe by the caller, used to
|
|
36
|
-
* skip the initial `sbx ls` here. Leave undefined to let this function
|
|
37
|
-
* probe on its own.
|
|
38
|
-
*/
|
|
39
|
-
alreadyExists?: boolean;
|
|
40
|
-
}
|
|
41
|
-
export declare function ensureSandbox(arguments_: EnsureSandboxArguments, signal?: AbortSignal): Promise<void>;
|
|
42
|
-
export {};
|
|
43
|
-
//# sourceMappingURL=dockerSandbox.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dockerSandbox.d.ts","sourceRoot":"","sources":["../../src/lib/dockerSandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGrD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpE;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;IAClB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,sBAAsB,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { runCommandAsync } from "./commandRunner.js";
|
|
2
|
-
import { applyGitDefaults } from "./sandboxGitDefaults.js";
|
|
3
|
-
/**
|
|
4
|
-
* Derive a deterministic sbx sandbox name from the sbx agent so every
|
|
5
|
-
* groundcrew model that targets the same agent reuses one sandbox across
|
|
6
|
-
* repositories and tickets. Lowercased and reduced to the sbx-safe
|
|
7
|
-
* charset (`a-z0-9.+-`) so unusual agent names still round-trip cleanly.
|
|
8
|
-
* Keep the `groundcrew-` prefix stable — doctor and teardown use it to
|
|
9
|
-
* identify groundcrew-owned sandboxes.
|
|
10
|
-
*/
|
|
11
|
-
export function sandboxNameFor(arguments_) {
|
|
12
|
-
const raw = `groundcrew-${arguments_.agent}`.toLowerCase();
|
|
13
|
-
return raw
|
|
14
|
-
.replaceAll(/[^a-z0-9.+-]+/g, "-")
|
|
15
|
-
.replaceAll(/-+/g, "-")
|
|
16
|
-
.replaceAll(/^-|-$/g, "");
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Probe `sbx ls` to see whether a sandbox with `sandboxName` already
|
|
20
|
-
* exists. Used by `crew sandbox auth` to switch between create vs reuse
|
|
21
|
-
* branches without surfacing the raw sbx error on first run.
|
|
22
|
-
*/
|
|
23
|
-
export async function sandboxExists(sandboxName, signal) {
|
|
24
|
-
const out = signal === undefined
|
|
25
|
-
? await runCommandAsync("sbx", ["ls"])
|
|
26
|
-
: await runCommandAsync("sbx", ["ls"], { signal });
|
|
27
|
-
return out.split("\n").some((line) => line.trim().split(/\s+/)[0] === sandboxName);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Idempotent guard: ensure a Docker Sandboxes container exists for the
|
|
31
|
-
* given repository + model. Probes `sbx ls`; if `sandboxName` is missing,
|
|
32
|
-
* calls `sbx create --name <name> [--template <t>] [--kit <k>]... <agent>
|
|
33
|
-
* <mountPath>` to provision it. Once the container exists (newly created
|
|
34
|
-
* or pre-existing), applies the standard git defaults when enabled.
|
|
35
|
-
* First-time agent auth still happens inside the sandbox the first time
|
|
36
|
-
* `sbx exec` runs the agent — `create` only provisions the container, it
|
|
37
|
-
* does not attach.
|
|
38
|
-
*/
|
|
39
|
-
async function resolveExistence(arguments_, signal) {
|
|
40
|
-
if (arguments_.alreadyExists === undefined) {
|
|
41
|
-
return await sandboxExists(arguments_.sandboxName, signal);
|
|
42
|
-
}
|
|
43
|
-
return arguments_.alreadyExists;
|
|
44
|
-
}
|
|
45
|
-
export async function ensureSandbox(arguments_, signal) {
|
|
46
|
-
const existed = await resolveExistence(arguments_, signal);
|
|
47
|
-
if (!existed) {
|
|
48
|
-
const createArguments = ["create", "--name", arguments_.sandboxName];
|
|
49
|
-
if (arguments_.sandbox.template !== undefined) {
|
|
50
|
-
createArguments.push("--template", arguments_.sandbox.template);
|
|
51
|
-
}
|
|
52
|
-
for (const kit of arguments_.sandbox.kits ?? []) {
|
|
53
|
-
createArguments.push("--kit", kit);
|
|
54
|
-
}
|
|
55
|
-
createArguments.push(arguments_.sandbox.agent, arguments_.mountPath);
|
|
56
|
-
const options = signal === undefined ? {} : { signal };
|
|
57
|
-
try {
|
|
58
|
-
await runCommandAsync("sbx", createArguments, options);
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (!(await sandboxExists(arguments_.sandboxName, signal))) {
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (arguments_.gitDefaults) {
|
|
67
|
-
await applyGitDefaults({ sandboxName: arguments_.sandboxName }, signal);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
interface ApplyGitDefaultsArguments {
|
|
2
|
-
sandboxName: string;
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* Apply the standard git defaults inside `sandboxName`. Idempotent —
|
|
6
|
-
* safe to call on every `ensure`/`auth` run to repair drift.
|
|
7
|
-
*/
|
|
8
|
-
export declare function applyGitDefaults(arguments_: ApplyGitDefaultsArguments, signal?: AbortSignal): Promise<void>;
|
|
9
|
-
export {};
|
|
10
|
-
//# sourceMappingURL=sandboxGitDefaults.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxGitDefaults.d.ts","sourceRoot":"","sources":["../../src/lib/sandboxGitDefaults.ts"],"names":[],"mappings":"AAyBA,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,yBAAyB,EACrC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAOf"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { runCommandAsync } from "./commandRunner.js";
|
|
2
|
-
/**
|
|
3
|
-
* Git defaults applied inside every sandbox when `sandbox.gitDefaults`
|
|
4
|
-
* is enabled (the default).
|
|
5
|
-
*
|
|
6
|
-
* - Disable GPG signing — robot commits inside the sandbox have no key
|
|
7
|
-
* and would otherwise fail or end up unsigned silently.
|
|
8
|
-
* - Rewrite GitHub SSH URLs to HTTPS so push/fetch go through the `gh`
|
|
9
|
-
* credential helper (wired by `gh auth setup-git` after a successful
|
|
10
|
-
* `crew sandbox auth` github login), regardless of how the user
|
|
11
|
-
* originally cloned the repo on the host.
|
|
12
|
-
*
|
|
13
|
-
* `url.<base>.insteadOf` is multi-valued in git; `--unset-all` before
|
|
14
|
-
* `--add` keeps the set identical across repeated runs instead of
|
|
15
|
-
* appending duplicates.
|
|
16
|
-
*/
|
|
17
|
-
const GIT_DEFAULT_COMMANDS = [
|
|
18
|
-
"git config --global commit.gpgsign false",
|
|
19
|
-
"git config --global tag.gpgsign false",
|
|
20
|
-
'(git config --global --unset-all url."https://github.com/".insteadOf 2>/dev/null || true)',
|
|
21
|
-
'git config --global --add url."https://github.com/".insteadOf "git@github.com:"',
|
|
22
|
-
'git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/"',
|
|
23
|
-
].join(" && ");
|
|
24
|
-
/**
|
|
25
|
-
* Apply the standard git defaults inside `sandboxName`. Idempotent —
|
|
26
|
-
* safe to call on every `ensure`/`auth` run to repair drift.
|
|
27
|
-
*/
|
|
28
|
-
export async function applyGitDefaults(arguments_, signal) {
|
|
29
|
-
const options = signal === undefined ? {} : { signal };
|
|
30
|
-
await runCommandAsync("sbx", ["exec", arguments_.sandboxName, "sh", "-c", GIT_DEFAULT_COMMANDS], options);
|
|
31
|
-
}
|