@clipboard-health/groundcrew 1.10.2 → 1.11.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 +3 -2
- package/dist/cli.js +1 -1
- package/dist/commands/remoteSetup.d.ts +1 -0
- package/dist/commands/remoteSetup.d.ts.map +1 -1
- package/dist/commands/remoteSetup.js +114 -0
- package/dist/lib/spriteRemoteRunnerProvider.d.ts +3 -0
- package/dist/lib/spriteRemoteRunnerProvider.d.ts.map +1 -1
- package/dist/lib/spriteRemoteRunnerProvider.js +99 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
81
81
|
--claude \
|
|
82
82
|
--codex \
|
|
83
83
|
--copy-local-codex-auth \
|
|
84
|
+
--datadog \
|
|
84
85
|
--github \
|
|
85
86
|
--git-name "Your Name" \
|
|
86
87
|
--git-email "you@users.noreply.github.com" \
|
|
@@ -89,7 +90,7 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
89
90
|
--checkpoint
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the remote runner if needed, prepares `~/dev`, configures Git, runs selected auth flows, adds selected MCP servers to Claude Code, and then opens Claude so you can run `/mcp` and authenticate only those selected servers. With the Sprite provider, `--copy-local-codex-auth` copies `${CODEX_HOME:-$HOME/.codex}/auth.json` into `/home/sprite/.codex/auth.json` and then verifies `codex login status`; it never prints the file contents. Use `--skip-mcp-auth` when you only want to add MCP definitions, and run the `/mcp` step later.
|
|
93
|
+
Known MCP aliases are `linear`, `slack`, and `notion`. For another HTTP MCP server, pass `--mcp name=https://example.com/mcp`. The command creates the remote runner if needed, prepares `~/dev`, configures Git, runs selected auth flows, adds selected MCP servers to Claude Code, and then opens Claude so you can run `/mcp` and authenticate only those selected servers. With the Sprite provider, `--copy-local-codex-auth` copies `${CODEX_HOME:-$HOME/.codex}/auth.json` into `/home/sprite/.codex/auth.json` and then verifies `codex login status`; it never prints the file contents. `--datadog` installs a pinned `pup` release in the remote runner, verifies its checksum, installs the `dd-pup` guidance for Claude and Codex, verifies `pup auth status`, and when auth is missing temporarily runs `sprite proxy` for the OAuth callback while `pup auth login --read-only` runs in the remote runner. Open the Datadog URL printed by `pup` if your terminal does not open it automatically. Use `--skip-mcp-auth` when you only want to add MCP definitions, and run the `/mcp` step later.
|
|
93
94
|
|
|
94
95
|
Repo setup is separate from runner setup and should run after the ticket branch exists, immediately before launching an agent. It clones/fetches the repo in the remote runner, checks out the requested branch (creating it from the base branch when it does not exist on origin), forwards only build-time secrets for the dependency install, removes the temporary secret file, clears those env vars, and then exits. It uses groundcrew's remote setup command.
|
|
95
96
|
|
|
@@ -180,7 +181,7 @@ Rules:
|
|
|
180
181
|
## Manual commands
|
|
181
182
|
|
|
182
183
|
```bash
|
|
183
|
-
crew remote setup crew-claude-1 --claude --codex --copy-local-codex-auth --github --mcp linear --checkpoint
|
|
184
|
+
crew remote setup crew-claude-1 --claude --codex --copy-local-codex-auth --datadog --github --mcp linear --checkpoint
|
|
184
185
|
crew remote bootstrap crew-claude-1 core-utils --branch rocky-team-123
|
|
185
186
|
crew remote sessions
|
|
186
187
|
crew remote attach <session-id-or-command> --runner crew-claude-1
|
package/dist/cli.js
CHANGED
|
@@ -62,7 +62,7 @@ const SUBCOMMANDS = {
|
|
|
62
62
|
},
|
|
63
63
|
remote: {
|
|
64
64
|
summary: "Create, authenticate, bootstrap, and inspect a remote runner",
|
|
65
|
-
usage: "setup <runner-name> [--claude] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
65
|
+
usage: "setup <runner-name> [--claude] [--codex] [--datadog] [--github] [--mcp <alias|name=url>] [--checkpoint]\n" +
|
|
66
66
|
" → crew remote bootstrap <runner-name> <repo> [--branch <branch>]\n" +
|
|
67
67
|
" → crew remote sessions [<runner-name>]\n" +
|
|
68
68
|
" → crew remote attach <session-id-or-command> [--runner <runner-name>]\n" +
|
|
@@ -8,6 +8,7 @@ export interface RemoteSetupOptions {
|
|
|
8
8
|
shouldAuthenticateClaude: boolean;
|
|
9
9
|
shouldAuthenticateCodex: boolean;
|
|
10
10
|
shouldCopyLocalCodexAuth?: boolean;
|
|
11
|
+
shouldSetupDatadog?: boolean;
|
|
11
12
|
shouldAuthenticateGithub: boolean;
|
|
12
13
|
shouldAuthenticateMcp: boolean;
|
|
13
14
|
shouldCheckpoint: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remoteSetup.d.ts","sourceRoot":"","sources":["../../src/commands/remoteSetup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remoteSetup.d.ts","sourceRoot":"","sources":["../../src/commands/remoteSetup.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,OAAO,CAAC;IAClC,uBAAuB,EAAE,OAAO,CAAC;IACjC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wBAAwB,EAAE,OAAO,CAAC;IAClC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,4BAA4B,EAAE,OAAO,CAAC;IACtC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAy0BD,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B9F;AAUD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAItF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAItF;AAED,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhG;AAYD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgClF;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7D"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
5
|
import { BUILD_SECRET_NAMES, DEFAULT_REMOTE_SETUP_COMMAND, loadConfig, } from "../lib/config.js";
|
|
5
6
|
import { shellSingleQuote } from "../lib/shell.js";
|
|
6
7
|
import { getRemoteRunnerProvider, remoteConfigWithRunnerName, } from "../lib/spriteRemoteRunnerProvider.js";
|
|
@@ -14,6 +15,10 @@ const DEFAULT_CHECKPOINT_COMMENT = "groundcrew remote runner baseline: selected
|
|
|
14
15
|
const CLAUDE_SUBSCRIPTION_LOGIN_FLAG = ["--claude", "ai"].join("");
|
|
15
16
|
const DEFAULT_REPOSITORY_OWNER = "ClipboardHealth";
|
|
16
17
|
const DEFAULT_BASE_BRANCH = "main";
|
|
18
|
+
const DATADOG_PUP_VERSION = "0.63.0";
|
|
19
|
+
const DATADOG_OAUTH_CALLBACK_PORT = 8000;
|
|
20
|
+
const DATADOG_AUTH_STATUS_RETRY_ATTEMPTS = 5;
|
|
21
|
+
const DATADOG_AUTH_STATUS_RETRY_DELAY_MS = 250;
|
|
17
22
|
const REMOTE_SECRETS_FILE = "/tmp/groundcrew-build-secrets.env";
|
|
18
23
|
const REMOTE_CODEX_AUTH_UPLOAD_FILE = "/tmp/groundcrew-codex-auth.json";
|
|
19
24
|
const REMOTE_CODEX_AUTH_FILE = "/home/sprite/.codex/auth.json";
|
|
@@ -31,6 +36,7 @@ function usage() {
|
|
|
31
36
|
" --claude Authenticate Claude Code with a Claude subscription",
|
|
32
37
|
" --codex Authenticate Codex CLI",
|
|
33
38
|
" --copy-local-codex-auth With --codex, copy local CODEX_HOME auth.json into the remote runner",
|
|
39
|
+
" --datadog Install pup, add dd-pup skills, and authenticate Datadog",
|
|
34
40
|
" --github Authenticate gh for GitHub PRs",
|
|
35
41
|
" --mcp <alias|name=url> Add/authenticate one MCP server; repeat for multiple",
|
|
36
42
|
" Known aliases: linear, slack, notion",
|
|
@@ -134,6 +140,7 @@ function parseArguments(argv) {
|
|
|
134
140
|
let shouldAuthenticateClaude = false;
|
|
135
141
|
let shouldAuthenticateCodex = false;
|
|
136
142
|
let shouldCopyLocalCodexAuth = false;
|
|
143
|
+
let shouldSetupDatadog = false;
|
|
137
144
|
let shouldAuthenticateGithub = false;
|
|
138
145
|
let shouldAuthenticateMcp = true;
|
|
139
146
|
let shouldCheckpoint = false;
|
|
@@ -156,6 +163,10 @@ function parseArguments(argv) {
|
|
|
156
163
|
shouldCopyLocalCodexAuth = true;
|
|
157
164
|
continue;
|
|
158
165
|
}
|
|
166
|
+
if (argument === "--datadog") {
|
|
167
|
+
shouldSetupDatadog = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
159
170
|
if (argument === "--github") {
|
|
160
171
|
shouldAuthenticateGithub = true;
|
|
161
172
|
continue;
|
|
@@ -202,6 +213,7 @@ function parseArguments(argv) {
|
|
|
202
213
|
shouldAuthenticateClaude,
|
|
203
214
|
shouldAuthenticateCodex,
|
|
204
215
|
shouldCopyLocalCodexAuth,
|
|
216
|
+
shouldSetupDatadog,
|
|
205
217
|
shouldAuthenticateGithub,
|
|
206
218
|
shouldAuthenticateMcp,
|
|
207
219
|
shouldCheckpoint,
|
|
@@ -457,6 +469,105 @@ async function authenticateCodex(arguments_) {
|
|
|
457
469
|
}
|
|
458
470
|
throw new Error("Codex login finished, but `codex login status` still reports not logged in inside the remote runner. Try `crew remote setup <runner-name> --codex --copy-local-codex-auth`.");
|
|
459
471
|
}
|
|
472
|
+
function datadogPupInstallCommand() {
|
|
473
|
+
const shellVariable = "$";
|
|
474
|
+
return [
|
|
475
|
+
"set -euo pipefail",
|
|
476
|
+
`version=${shellSingleQuote(DATADOG_PUP_VERSION)}`,
|
|
477
|
+
'case "$(uname -m)" in',
|
|
478
|
+
" x86_64|amd64) arch=x86_64 ;;",
|
|
479
|
+
" aarch64|arm64) arch=arm64 ;;",
|
|
480
|
+
' *) echo "Unsupported Linux architecture: $(uname -m)" >&2; exit 1 ;;',
|
|
481
|
+
"esac",
|
|
482
|
+
`asset="pup_${shellVariable}{version}_Linux_${shellVariable}{arch}.tar.gz"`,
|
|
483
|
+
`base_url="https://github.com/DataDog/pup/releases/download/v${shellVariable}{version}"`,
|
|
484
|
+
'binary="$HOME/.local/bin/pup"',
|
|
485
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
486
|
+
`if [ -x "$binary" ] && "$binary" version | grep -q "Pup ${shellVariable}{version} "; then "$binary" version; exit 0; fi`,
|
|
487
|
+
'install_dir="$(mktemp -d)"',
|
|
488
|
+
"trap 'rm -rf \"$install_dir\"' EXIT",
|
|
489
|
+
`archive="${shellVariable}{install_dir}/${shellVariable}{asset}"`,
|
|
490
|
+
`checksums="${shellVariable}{install_dir}/pup_${shellVariable}{version}_checksums.txt"`,
|
|
491
|
+
'curl -fsSL -o "$archive" "$base_url/$asset"',
|
|
492
|
+
`curl -fsSL -o "$checksums" "$base_url/pup_${shellVariable}{version}_checksums.txt"`,
|
|
493
|
+
`(cd "$install_dir" && grep " ${shellVariable}{asset}$" "$checksums" | sha256sum -c -)`,
|
|
494
|
+
'tar -xzf "$archive" -C "$HOME/.local/bin" pup',
|
|
495
|
+
'chmod 755 "$binary"',
|
|
496
|
+
'"$binary" version',
|
|
497
|
+
].join("\n");
|
|
498
|
+
}
|
|
499
|
+
async function installDatadogPup(provider, config) {
|
|
500
|
+
log(`Installing Datadog pup ${DATADOG_PUP_VERSION} inside the remote runner`);
|
|
501
|
+
await provider.runCommand({
|
|
502
|
+
config,
|
|
503
|
+
remoteArguments: ["bash", "-lc", datadogPupInstallCommand()],
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async function installDatadogPupSkills(provider, config) {
|
|
507
|
+
for (const platform of ["claude-code", "codex"]) {
|
|
508
|
+
// oxlint-disable-next-line no-await-in-loop -- installs are quick and ordered so logs stay readable.
|
|
509
|
+
await provider.runCommand({
|
|
510
|
+
config,
|
|
511
|
+
remoteArguments: ["pup", "skills", "install", platform, "--name", "dd-pup", "--yes"],
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function validateDatadogLogin(provider, config) {
|
|
516
|
+
return await commandSucceeds({
|
|
517
|
+
provider,
|
|
518
|
+
config,
|
|
519
|
+
remoteArguments: ["pup", "auth", "status"],
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async function waitForDatadogLogin(provider, config) {
|
|
523
|
+
for (let attempt = 1; attempt <= DATADOG_AUTH_STATUS_RETRY_ATTEMPTS; attempt += 1) {
|
|
524
|
+
// oxlint-disable-next-line no-await-in-loop -- bounded polling smooths over Datadog OAuth propagation delay.
|
|
525
|
+
if (await validateDatadogLogin(provider, config)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (attempt < DATADOG_AUTH_STATUS_RETRY_ATTEMPTS) {
|
|
529
|
+
// oxlint-disable-next-line no-await-in-loop -- retries are intentionally spaced.
|
|
530
|
+
await sleep(DATADOG_AUTH_STATUS_RETRY_DELAY_MS);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
async function authenticateDatadog(provider, config) {
|
|
536
|
+
if (await validateDatadogLogin(provider, config)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
writeOutput();
|
|
540
|
+
writeOutput("Datadog OAuth will run inside the remote runner.");
|
|
541
|
+
writeOutput(`Groundcrew is forwarding local port ${DATADOG_OAUTH_CALLBACK_PORT} to the remote callback server while login runs.`);
|
|
542
|
+
writeOutput("Open the Datadog URL printed by pup if your terminal does not open it for you.");
|
|
543
|
+
writeOutput();
|
|
544
|
+
const proxy = await provider.startPortProxy(config, DATADOG_OAUTH_CALLBACK_PORT);
|
|
545
|
+
try {
|
|
546
|
+
await provider.runTtyCommand({
|
|
547
|
+
config,
|
|
548
|
+
remoteArguments: [
|
|
549
|
+
"pup",
|
|
550
|
+
"auth",
|
|
551
|
+
"login",
|
|
552
|
+
"--read-only",
|
|
553
|
+
"--callback-port",
|
|
554
|
+
String(DATADOG_OAUTH_CALLBACK_PORT),
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
await proxy.close();
|
|
560
|
+
}
|
|
561
|
+
if (await waitForDatadogLogin(provider, config)) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
throw new Error("Datadog auth finished, but `pup auth status` still reports not logged in inside the remote runner.");
|
|
565
|
+
}
|
|
566
|
+
async function setupDatadog(provider, config) {
|
|
567
|
+
await installDatadogPup(provider, config);
|
|
568
|
+
await installDatadogPupSkills(provider, config);
|
|
569
|
+
await authenticateDatadog(provider, config);
|
|
570
|
+
}
|
|
460
571
|
async function addMcpServer(arguments_) {
|
|
461
572
|
const { provider, config, server } = arguments_;
|
|
462
573
|
const exists = await commandSucceeds({
|
|
@@ -661,6 +772,9 @@ export async function setupRemoteRunner(options) {
|
|
|
661
772
|
shouldCopyLocalAuth: options.shouldCopyLocalCodexAuth === true,
|
|
662
773
|
});
|
|
663
774
|
}
|
|
775
|
+
if (options.shouldSetupDatadog === true) {
|
|
776
|
+
await setupDatadog(provider, config);
|
|
777
|
+
}
|
|
664
778
|
for (const server of options.mcpServers) {
|
|
665
779
|
// oxlint-disable-next-line no-await-in-loop -- MCP additions are sequential so auth instructions stay ordered.
|
|
666
780
|
await addMcpServer({ provider, config, server });
|
|
@@ -54,6 +54,9 @@ export interface RemoteRunnerProvider {
|
|
|
54
54
|
runCommand(arguments_: RemoteRunArguments): Promise<string | undefined>;
|
|
55
55
|
runTtyCommand(arguments_: RemoteRunArguments): Promise<void>;
|
|
56
56
|
buildTtyCommand(arguments_: RemoteTtyCommandArguments): string;
|
|
57
|
+
startPortProxy(config: RemoteRunnerConfig, port: number): Promise<{
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}>;
|
|
57
60
|
listSessions(config: RemoteRunnerConfig): Promise<string>;
|
|
58
61
|
attachSession(config: RemoteRunnerConfig, target: string): Promise<void>;
|
|
59
62
|
listProcesses(config: RemoteRunnerConfig): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spriteRemoteRunnerProvider.d.ts","sourceRoot":"","sources":["../../src/lib/spriteRemoteRunnerProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spriteRemoteRunnerProvider.d.ts","sourceRoot":"","sources":["../../src/lib/spriteRemoteRunnerProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
1
3
|
import { runCommandAsync } from "./commandRunner.js";
|
|
2
4
|
import { shellSingleQuote } from "./shell.js";
|
|
3
5
|
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
6
|
+
const PROXY_CLOSE_SIGNAL = "SIGINT";
|
|
7
|
+
const PROXY_READY_HOST = "127.0.0.1";
|
|
8
|
+
const PROXY_READY_TIMEOUT_MS = 5000;
|
|
9
|
+
const PROXY_READY_RETRY_DELAY_MS = 25;
|
|
4
10
|
export const SPRITE_REMOTE_PROVIDER_DEFAULTS = {
|
|
5
11
|
provider: "sprite",
|
|
6
12
|
runnerName: "crew-claude-1",
|
|
@@ -155,6 +161,96 @@ async function createSpriteRunner(config) {
|
|
|
155
161
|
timeoutMs: 0,
|
|
156
162
|
});
|
|
157
163
|
}
|
|
164
|
+
async function startSpritePortProxy(config, port) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
let closeWasRequested = false;
|
|
167
|
+
let proxyError;
|
|
168
|
+
const proxy = (async () => {
|
|
169
|
+
try {
|
|
170
|
+
await runCommandAsync("sprite", ["proxy", "-s", config.runnerName, String(port)], {
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
stdio: "inherit",
|
|
173
|
+
timeoutMs: 0,
|
|
174
|
+
});
|
|
175
|
+
if (!closeWasRequested) {
|
|
176
|
+
proxyError = new Error("Sprite proxy exited before it was closed.");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (closeWasRequested && errorHasSignal(error, PROXY_CLOSE_SIGNAL)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
proxyError = new Error(`Sprite proxy exited before it was closed: ${String(error)}`, {
|
|
184
|
+
cause: error,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
try {
|
|
189
|
+
await waitForSpritePortProxy({ port, proxy, proxyError: () => proxyError });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
closeWasRequested = true;
|
|
193
|
+
controller.abort();
|
|
194
|
+
await proxy;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
async close() {
|
|
199
|
+
closeWasRequested = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
await proxy;
|
|
202
|
+
if (proxyError !== undefined) {
|
|
203
|
+
throw new Error(proxyError.message, { cause: proxyError });
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function waitForSpritePortProxy(arguments_) {
|
|
209
|
+
const deadline = Date.now() + PROXY_READY_TIMEOUT_MS;
|
|
210
|
+
while (Date.now() < deadline) {
|
|
211
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
212
|
+
// eslint-disable-next-line no-await-in-loop -- readiness polling must observe attempts sequentially.
|
|
213
|
+
if (await canConnectToLocalPort(arguments_.port)) {
|
|
214
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
218
|
+
// eslint-disable-next-line no-await-in-loop -- retry delay is bounded and stops early if the proxy exits.
|
|
219
|
+
await Promise.race([sleep(PROXY_READY_RETRY_DELAY_MS), arguments_.proxy]);
|
|
220
|
+
}
|
|
221
|
+
throwIfProxyExited(arguments_.proxyError());
|
|
222
|
+
throw new Error(`Timed out waiting for Sprite proxy on ${PROXY_READY_HOST}:${arguments_.port} to accept connections.`);
|
|
223
|
+
}
|
|
224
|
+
function throwIfProxyExited(error) {
|
|
225
|
+
if (error !== undefined) {
|
|
226
|
+
throw new Error(error.message, { cause: error });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function canConnectToLocalPort(port) {
|
|
230
|
+
return await new Promise((resolve) => {
|
|
231
|
+
const socket = connect({ host: PROXY_READY_HOST, port });
|
|
232
|
+
socket.once("connect", () => {
|
|
233
|
+
socket.destroy();
|
|
234
|
+
resolve(true);
|
|
235
|
+
});
|
|
236
|
+
socket.once("error", () => {
|
|
237
|
+
socket.destroy();
|
|
238
|
+
resolve(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function errorHasSignal(error, signal) {
|
|
243
|
+
if (typeof error !== "object" || error === null) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if ("signal" in error && error.signal === signal) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof Error && error.cause !== undefined) {
|
|
250
|
+
return errorHasSignal(error.cause, signal);
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
158
254
|
export const spriteRemoteRunnerProvider = {
|
|
159
255
|
name: "sprite",
|
|
160
256
|
async runnerExists(config) {
|
|
@@ -174,6 +270,9 @@ export const spriteRemoteRunnerProvider = {
|
|
|
174
270
|
});
|
|
175
271
|
},
|
|
176
272
|
buildTtyCommand: buildSpriteTtyCommand,
|
|
273
|
+
async startPortProxy(config, port) {
|
|
274
|
+
return await startSpritePortProxy(config, port);
|
|
275
|
+
},
|
|
177
276
|
async listSessions(config) {
|
|
178
277
|
const output = await runCommandAsync("sprite", ["sessions", "list", "-s", config.runnerName], {
|
|
179
278
|
trim: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle, remote runners, and usage tracking.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|