@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.
Files changed (62) hide show
  1. package/README.md +32 -13
  2. package/crew.config.example.ts +5 -18
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +64 -10
  5. package/dist/commands/interruptWorkspace.d.ts.map +1 -1
  6. package/dist/commands/interruptWorkspace.js +3 -3
  7. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  8. package/dist/commands/resumeWorkspace.js +1 -2
  9. package/dist/commands/setupRepos.d.ts.map +1 -1
  10. package/dist/commands/setupRepos.js +2 -13
  11. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  12. package/dist/commands/setupWorkspace.js +1 -7
  13. package/dist/lib/agentLaunch.d.ts +0 -6
  14. package/dist/lib/agentLaunch.d.ts.map +1 -1
  15. package/dist/lib/agentLaunch.js +1 -12
  16. package/dist/lib/cmuxAdapter.d.ts +8 -0
  17. package/dist/lib/cmuxAdapter.d.ts.map +1 -0
  18. package/dist/lib/cmuxAdapter.js +163 -0
  19. package/dist/lib/config.d.ts +2 -76
  20. package/dist/lib/config.d.ts.map +1 -1
  21. package/dist/lib/config.js +29 -102
  22. package/dist/lib/launchCommand.d.ts +3 -3
  23. package/dist/lib/sandboxName.d.ts +9 -0
  24. package/dist/lib/sandboxName.d.ts.map +1 -0
  25. package/dist/lib/sandboxName.js +12 -0
  26. package/dist/lib/tmuxAdapter.d.ts +9 -0
  27. package/dist/lib/tmuxAdapter.d.ts.map +1 -0
  28. package/dist/lib/tmuxAdapter.js +156 -0
  29. package/dist/lib/util.d.ts +11 -0
  30. package/dist/lib/util.d.ts.map +1 -1
  31. package/dist/lib/util.js +21 -0
  32. package/dist/lib/workspaceAdapter.d.ts +79 -0
  33. package/dist/lib/workspaceAdapter.d.ts.map +1 -0
  34. package/dist/lib/workspaceAdapter.js +17 -0
  35. package/dist/lib/workspaces.d.ts +7 -55
  36. package/dist/lib/workspaces.d.ts.map +1 -1
  37. package/dist/lib/workspaces.js +8 -404
  38. package/package.json +1 -2
  39. package/dist/commands/sandbox/auth.d.ts +0 -3
  40. package/dist/commands/sandbox/auth.d.ts.map +0 -1
  41. package/dist/commands/sandbox/auth.js +0 -227
  42. package/dist/commands/sandbox/index.d.ts +0 -2
  43. package/dist/commands/sandbox/index.d.ts.map +0 -1
  44. package/dist/commands/sandbox/index.js +0 -47
  45. package/dist/commands/sandbox/inspect.d.ts +0 -2
  46. package/dist/commands/sandbox/inspect.d.ts.map +0 -1
  47. package/dist/commands/sandbox/inspect.js +0 -18
  48. package/dist/commands/sandbox/lifecycle.d.ts +0 -7
  49. package/dist/commands/sandbox/lifecycle.d.ts.map +0 -1
  50. package/dist/commands/sandbox/lifecycle.js +0 -68
  51. package/dist/commands/sandbox/model.d.ts +0 -10
  52. package/dist/commands/sandbox/model.d.ts.map +0 -1
  53. package/dist/commands/sandbox/model.js +0 -37
  54. package/dist/commands/sandbox/picker.d.ts +0 -20
  55. package/dist/commands/sandbox/picker.d.ts.map +0 -1
  56. package/dist/commands/sandbox/picker.js +0 -23
  57. package/dist/lib/dockerSandbox.d.ts +0 -43
  58. package/dist/lib/dockerSandbox.d.ts.map +0 -1
  59. package/dist/lib/dockerSandbox.js +0 -69
  60. package/dist/lib/sandboxGitDefaults.d.ts +0 -10
  61. package/dist/lib/sandboxGitDefaults.d.ts.map +0 -1
  62. 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,2 +0,0 @@
1
- export declare function sandboxCli(argv: string[]): Promise<void>;
2
- //# sourceMappingURL=index.d.ts.map
@@ -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,2 +0,0 @@
1
- export declare function runList(): Promise<void>;
2
- //# sourceMappingURL=inspect.d.ts.map
@@ -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
- }