@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.
Files changed (64) hide show
  1. package/README.md +37 -13
  2. package/crew.config.example.ts +5 -18
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +27 -49
  5. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  6. package/dist/commands/resumeWorkspace.js +1 -2
  7. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  8. package/dist/commands/setupWorkspace.js +1 -7
  9. package/dist/commands/upgrade.d.ts +0 -11
  10. package/dist/commands/upgrade.d.ts.map +1 -1
  11. package/dist/commands/upgrade.js +14 -100
  12. package/dist/lib/agentLaunch.d.ts +0 -6
  13. package/dist/lib/agentLaunch.d.ts.map +1 -1
  14. package/dist/lib/agentLaunch.js +1 -12
  15. package/dist/lib/cmuxAdapter.d.ts +8 -0
  16. package/dist/lib/cmuxAdapter.d.ts.map +1 -0
  17. package/dist/lib/cmuxAdapter.js +163 -0
  18. package/dist/lib/config.d.ts +2 -76
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +29 -102
  21. package/dist/lib/launchCommand.d.ts +3 -3
  22. package/dist/lib/sandboxName.d.ts +9 -0
  23. package/dist/lib/sandboxName.d.ts.map +1 -0
  24. package/dist/lib/sandboxName.js +12 -0
  25. package/dist/lib/tmuxAdapter.d.ts +9 -0
  26. package/dist/lib/tmuxAdapter.d.ts.map +1 -0
  27. package/dist/lib/tmuxAdapter.js +156 -0
  28. package/dist/lib/workspaceAdapter.d.ts +79 -0
  29. package/dist/lib/workspaceAdapter.d.ts.map +1 -0
  30. package/dist/lib/workspaceAdapter.js +17 -0
  31. package/dist/lib/workspaces.d.ts +7 -55
  32. package/dist/lib/workspaces.d.ts.map +1 -1
  33. package/dist/lib/workspaces.js +8 -404
  34. package/package.json +1 -2
  35. package/dist/commands/sandbox/auth.d.ts +0 -3
  36. package/dist/commands/sandbox/auth.d.ts.map +0 -1
  37. package/dist/commands/sandbox/auth.js +0 -227
  38. package/dist/commands/sandbox/index.d.ts +0 -2
  39. package/dist/commands/sandbox/index.d.ts.map +0 -1
  40. package/dist/commands/sandbox/index.js +0 -47
  41. package/dist/commands/sandbox/inspect.d.ts +0 -2
  42. package/dist/commands/sandbox/inspect.d.ts.map +0 -1
  43. package/dist/commands/sandbox/inspect.js +0 -18
  44. package/dist/commands/sandbox/lifecycle.d.ts +0 -7
  45. package/dist/commands/sandbox/lifecycle.d.ts.map +0 -1
  46. package/dist/commands/sandbox/lifecycle.js +0 -68
  47. package/dist/commands/sandbox/model.d.ts +0 -10
  48. package/dist/commands/sandbox/model.d.ts.map +0 -1
  49. package/dist/commands/sandbox/model.js +0 -37
  50. package/dist/commands/sandbox/picker.d.ts +0 -20
  51. package/dist/commands/sandbox/picker.d.ts.map +0 -1
  52. package/dist/commands/sandbox/picker.js +0 -23
  53. package/dist/commands/setupRepos.d.ts +0 -44
  54. package/dist/commands/setupRepos.d.ts.map +0 -1
  55. package/dist/commands/setupRepos.js +0 -212
  56. package/dist/lib/dockerSandbox.d.ts +0 -43
  57. package/dist/lib/dockerSandbox.d.ts.map +0 -1
  58. package/dist/lib/dockerSandbox.js +0 -69
  59. package/dist/lib/sandboxGitDefaults.d.ts +0 -10
  60. package/dist/lib/sandboxGitDefaults.d.ts.map +0 -1
  61. package/dist/lib/sandboxGitDefaults.js +0 -31
  62. package/dist/lib/upgrade.d.ts +0 -66
  63. package/dist/lib/upgrade.d.ts.map +0 -1
  64. package/dist/lib/upgrade.js +0 -178
@@ -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,44 +0,0 @@
1
- /**
2
- * `crew setup repos` — clone every entry of `workspace.knownRepositories`
3
- * that does not already exist under `workspace.projectDir`. Entries
4
- * shaped `<owner>/<repo>` are cloned via `gh repo clone`; bare-name
5
- * entries are skipped with a hint, because they have no canonical URL
6
- * we can guess at without involving the user's gh login. Idempotent.
7
- */
8
- import { type ResolvedConfig } from "../lib/config.ts";
9
- export interface SetupReposOptions {
10
- /** Print the plan without running any clone. */
11
- dryRun?: boolean;
12
- /**
13
- * Restrict the action to this subset of `knownRepositories`. Each entry
14
- * must match an entry in the config or the call rejects before any side
15
- * effect.
16
- */
17
- only?: readonly string[];
18
- }
19
- export type SetupReposSkipKind = "bare-name" | "invalid-repository" | "invalid-target";
20
- export interface SetupReposSkip {
21
- repo: string;
22
- kind: SetupReposSkipKind;
23
- reason: string;
24
- }
25
- export interface SetupReposResult {
26
- /** Entries already present under `projectDir`. */
27
- existing: string[];
28
- /** Entries that would be cloned in dry-run mode. */
29
- planned: string[];
30
- /** Entries successfully cloned this run. */
31
- cloned: string[];
32
- /** Entries skipped with a reason (e.g. bare names, invalid targets). */
33
- skipped: SetupReposSkip[];
34
- /** Entries that failed during clone. */
35
- failed: {
36
- repo: string;
37
- error: Error;
38
- }[];
39
- /** True when `gh` is missing and at least one clone was needed. */
40
- ghMissing: boolean;
41
- }
42
- export declare function setupRepos(config: ResolvedConfig, options: SetupReposOptions): Promise<SetupReposResult>;
43
- export declare function setupReposCli(argv: string[]): Promise<void>;
44
- //# sourceMappingURL=setupRepos.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAcD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
@@ -1,212 +0,0 @@
1
- /**
2
- * `crew setup repos` — clone every entry of `workspace.knownRepositories`
3
- * that does not already exist under `workspace.projectDir`. Entries
4
- * shaped `<owner>/<repo>` are cloned via `gh repo clone`; bare-name
5
- * entries are skipped with a hint, because they have no canonical URL
6
- * we can guess at without involving the user's gh login. Idempotent.
7
- */
8
- import { mkdirSync, opendirSync, statSync } from "node:fs";
9
- import { dirname, isAbsolute, relative, resolve } from "node:path";
10
- import { runCommandAsync } from "../lib/commandRunner.js";
11
- import { loadConfig } from "../lib/config.js";
12
- import { which } from "../lib/host.js";
13
- import { errorMessage, log, parseDryRunPositionals, writeOutput } from "../lib/util.js";
14
- function emptyResult() {
15
- return {
16
- existing: [],
17
- planned: [],
18
- cloned: [],
19
- skipped: [],
20
- failed: [],
21
- ghMissing: false,
22
- };
23
- }
24
- function selectRepositories(config, only) {
25
- if (only === undefined) {
26
- return config.workspace.knownRepositories;
27
- }
28
- const known = new Set(config.workspace.knownRepositories);
29
- const unknown = only.filter((entry) => !known.has(entry));
30
- if (unknown.length > 0) {
31
- throw new Error(`Repositories not in workspace.knownRepositories: ${unknown.join(", ")}. Known: ${config.workspace.knownRepositories.join(", ")}`);
32
- }
33
- return only;
34
- }
35
- function pathExists(path) {
36
- return statSync(path, { throwIfNoEntry: false }) !== undefined;
37
- }
38
- function isDirectoryEmpty(path) {
39
- const directory = opendirSync(path);
40
- try {
41
- return directory.readSync() === null;
42
- }
43
- finally {
44
- directory.closeSync();
45
- }
46
- }
47
- function existingTargetPlan(target) {
48
- const stats = statSync(target, { throwIfNoEntry: false });
49
- if (stats === undefined) {
50
- return "clone";
51
- }
52
- if (!stats.isDirectory()) {
53
- return "skip-invalid";
54
- }
55
- if (pathExists(resolve(target, ".git"))) {
56
- return "existing";
57
- }
58
- return isDirectoryEmpty(target) ? "clone" : "skip-invalid";
59
- }
60
- function isInsideProjectDir(projectDir, target) {
61
- const relativeTarget = relative(projectDir, target);
62
- return (relativeTarget.length > 0 && !relativeTarget.startsWith("..") && !isAbsolute(relativeTarget));
63
- }
64
- function repositoryEntryPlan(repo) {
65
- const parts = repo.split("/");
66
- if (parts.length === 1) {
67
- return "bare-name";
68
- }
69
- if (parts.length === 2 && parts.every((part) => part.length > 0)) {
70
- return "clone";
71
- }
72
- return "invalid-repository";
73
- }
74
- function bareNameSkip(repo, target) {
75
- return {
76
- repo,
77
- kind: "bare-name",
78
- reason: `bare name needs owner/ prefix to auto-clone; clone manually into ${target}`,
79
- };
80
- }
81
- function invalidTargetSkip(repo, target) {
82
- return {
83
- repo,
84
- kind: "invalid-target",
85
- reason: `target exists but is not a git repository or empty directory: ${target}`,
86
- };
87
- }
88
- function invalidRepositorySkip(repo, target) {
89
- return {
90
- repo,
91
- kind: "invalid-repository",
92
- reason: `repository must be owner/repo to auto-clone; clone manually into ${target}`,
93
- };
94
- }
95
- function escapingTargetSkip(repo, projectDir, target) {
96
- return {
97
- repo,
98
- kind: "invalid-repository",
99
- reason: `repository resolves outside workspace.projectDir (${projectDir}): ${target}`,
100
- };
101
- }
102
- function planClones(config, repositories) {
103
- const projectDir = resolve(config.workspace.projectDir);
104
- const toClone = [];
105
- const existing = [];
106
- const skipped = [];
107
- const seen = new Set();
108
- for (const entry of repositories) {
109
- if (seen.has(entry)) {
110
- continue;
111
- }
112
- seen.add(entry);
113
- const target = resolve(projectDir, entry);
114
- if (!isInsideProjectDir(projectDir, target)) {
115
- skipped.push(escapingTargetSkip(entry, projectDir, target));
116
- continue;
117
- }
118
- const targetPlan = existingTargetPlan(target);
119
- if (targetPlan === "existing") {
120
- existing.push(entry);
121
- continue;
122
- }
123
- if (targetPlan === "skip-invalid") {
124
- skipped.push(invalidTargetSkip(entry, target));
125
- continue;
126
- }
127
- const repositoryPlan = repositoryEntryPlan(entry);
128
- if (repositoryPlan === "bare-name") {
129
- skipped.push(bareNameSkip(entry, target));
130
- continue;
131
- }
132
- if (repositoryPlan === "invalid-repository") {
133
- skipped.push(invalidRepositorySkip(entry, target));
134
- continue;
135
- }
136
- toClone.push(entry);
137
- }
138
- return { toClone, existing, skipped };
139
- }
140
- export async function setupRepos(config, options) {
141
- const repositories = selectRepositories(config, options.only);
142
- const plan = planClones(config, repositories);
143
- const result = emptyResult();
144
- result.existing = plan.existing;
145
- result.skipped = plan.skipped;
146
- for (const entry of plan.existing) {
147
- log(`[exists] ${entry}`);
148
- }
149
- for (const { repo, reason } of plan.skipped) {
150
- log(`[skip] ${repo} — ${reason}`);
151
- }
152
- if (options.dryRun === true) {
153
- result.planned = plan.toClone;
154
- for (const entry of plan.toClone) {
155
- log(`[dry-run] would clone ${entry}`);
156
- }
157
- return result;
158
- }
159
- if (plan.toClone.length === 0) {
160
- return result;
161
- }
162
- const ghPath = await which("gh");
163
- if (ghPath === undefined) {
164
- result.ghMissing = true;
165
- writeOutput("gh CLI not found - install GitHub CLI from https://cli.github.com/ (or clone the missing repos manually).");
166
- return result;
167
- }
168
- const projectDir = resolve(config.workspace.projectDir);
169
- // Sequential on purpose: each `gh repo clone` inherits stdio for progress
170
- // bars and auth prompts. Parallel clones would interleave output and make
171
- // any interactive 2FA prompt unanswerable.
172
- for (const entry of plan.toClone) {
173
- const target = resolve(projectDir, entry);
174
- log(`[clone] ${entry} → ${target}`);
175
- try {
176
- mkdirSync(dirname(target), { recursive: true });
177
- // oxlint-disable-next-line no-await-in-loop -- see comment above
178
- await runCommandAsync("gh", ["repo", "clone", entry, target], {
179
- stdio: "inherit",
180
- timeoutMs: 0,
181
- });
182
- result.cloned.push(entry);
183
- }
184
- catch (error) {
185
- const wrapped = error instanceof Error ? error : new Error(errorMessage(error));
186
- log(`[fail] ${entry}: ${wrapped.message}`);
187
- result.failed.push({ repo: entry, error: wrapped });
188
- }
189
- }
190
- return result;
191
- }
192
- function parseArguments(argv) {
193
- const { dryRun, positionals } = parseDryRunPositionals(argv, "crew setup repos [--dry-run] [<repo>...]");
194
- const options = { dryRun };
195
- if (positionals.length > 0) {
196
- options.only = positionals;
197
- }
198
- return options;
199
- }
200
- export async function setupReposCli(argv) {
201
- const options = parseArguments(argv);
202
- const config = await loadConfig();
203
- const result = await setupRepos(config, options);
204
- if (result.ghMissing || result.failed.length > 0) {
205
- process.exitCode = 1;
206
- return;
207
- }
208
- // Remaining skips mean setup is incomplete — signal that to CI gates.
209
- if (result.skipped.length > 0) {
210
- process.exitCode = 1;
211
- }
212
- }
@@ -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"}