@clipboard-health/groundcrew 4.3.5 → 4.5.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 +64 -22
- package/crew.config.example.ts +10 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +49 -16
- package/dist/lib/agentLaunch.d.ts +1 -1
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +18 -0
- package/dist/lib/config.d.ts +35 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +83 -23
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +119 -28
- package/dist/lib/tmuxAdapter.d.ts.map +1 -1
- package/dist/lib/tmuxAdapter.js +10 -6
- package/dist/lib/workspaceAdapter.d.ts +4 -1
- package/dist/lib/workspaceAdapter.d.ts.map +1 -1
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,27 +130,69 @@ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any
|
|
|
130
130
|
<details>
|
|
131
131
|
<summary>Full configuration reference</summary>
|
|
132
132
|
|
|
133
|
-
| Key
|
|
134
|
-
|
|
|
135
|
-
| `sources`
|
|
136
|
-
| `git.remote`
|
|
137
|
-
| `git.defaultBranch`
|
|
138
|
-
| `workspace.projectDir`
|
|
139
|
-
| `workspace.knownRepositories`
|
|
140
|
-
| `orchestrator.maximumInProgress`
|
|
141
|
-
| `orchestrator.pollIntervalMilliseconds`
|
|
142
|
-
| `orchestrator.sessionLimitPercentage`
|
|
143
|
-
| `models.default`
|
|
144
|
-
| `models.definitions`
|
|
145
|
-
| `models.definitions.<name>.cmd`
|
|
146
|
-
| `models.definitions.<name>.color`
|
|
147
|
-
| `models.definitions.<name>.usage`
|
|
148
|
-
| `models.definitions.<name>.sandbox`
|
|
149
|
-
| `models.definitions.<name>.
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
133
|
+
| Key | Default | What it does |
|
|
134
|
+
| ---------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| `sources` | `[]` | Additional pluggable ticket sources, dispatched alongside the built-in Linear adapter. Built-in kinds: `shell`, `linear`. |
|
|
136
|
+
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
|
|
137
|
+
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
|
|
138
|
+
| `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
|
|
139
|
+
| `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
|
|
140
|
+
| `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
|
|
141
|
+
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
142
|
+
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
143
|
+
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew start <TICKET>` for unlabeled tickets. `crew run` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
144
|
+
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
145
|
+
| `models.definitions.<name>.cmd` | — | Shell command launched for the model. Runs in the worktree through the resolved `local.runner`. `{{worktree}}` is replaced before launch; `{{sandbox}}` expands to the sbx sandbox name under the sdx runner and an empty string otherwise. |
|
|
146
|
+
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
147
|
+
| `models.definitions.<name>.usage` | optional | If set, codexbar usage is fetched for this model and gated by `sessionLimitPercentage`. Falls back to default when unset, with gating enabled for known models. When `usage.codexbar.source` is omitted, groundcrew uses `oauth` for Codex/Claude on macOS, `auto` for other macOS providers, and `cli` elsewhere. Set to `{ disabled: true }` to disable usage gating. |
|
|
148
|
+
| `models.definitions.<name>.sandbox` | optional | Docker Sandboxes binding for the model. Required at launch when `local.runner` resolves to `sdx`. Fields: `agent` (required sbx agent name) and `setupCommand` (override for the inside-sandbox setup script). Groundcrew assumes the `groundcrew-<agent>` sandbox already exists. |
|
|
149
|
+
| `models.definitions.<name>.preLaunch` | optional | Host-only shell snippet run **before** the agent exec and **outside** Safehouse/sdx. Exports survive into the launch shell; under the default `safehouse` runner they're only forwarded to the agent when listed via `preLaunchEnv` (recommended) or when `cmd` includes its own `safehouse --env-pass=NAMES`. `{{worktree}}` is substituted. A non-zero exit aborts launch. Not supported when `local.runner` resolves to `sdx` (v1). |
|
|
150
|
+
| `models.definitions.<name>.preLaunchEnv` | optional | Companion to `preLaunch`: list of env var names to append to groundcrew's `safehouse-clearance` `--env-pass=` flag, so `preLaunch` exports reach the agent **without** overriding `cmd` and losing the project's egress allowlist. Each entry must match `[A-Za-z_][A-Za-z0-9_]*`. Under `runner: "none"` exports already inherit and `preLaunchEnv` is a no-op. An empty array is a uniform no-op in every runner; a **non-empty** list is rejected when `cmd` already starts with `safehouse` (user owns env forwarding) or when `runner` resolves to `sdx`. |
|
|
151
|
+
| `models.definitions.<name>.disabled` | optional | When set to exactly `true`, drops the named shipped default (`claude` or `codex`). Doctor skips probing it; `agent-<name>` labels fall back to `models.default` with a warning. |
|
|
152
|
+
| `prompts.initial` | unattended template | First message sent to the agent. Placeholders: `{{ticket}}`, `{{worktree}}`, `{{title}}`, `{{description}}`. Override this from `crew.config.ts` for team-specific statuses, tools, plugins, or review loops. |
|
|
153
|
+
| `workspaceKind` | `"auto"` | Terminal session manager. `"auto"` picks `cmux` when on PATH, else `tmux`. Set to `"cmux"` or `"tmux"` to fail loudly when the chosen backend is missing. |
|
|
154
|
+
| `local.runner` | `"auto"` | Local isolation backend. `"auto"` → `safehouse` on macOS, `sdx` on Linux/WSL. Explicit: `"safehouse"`, `"sdx"`, `"none"`. `"none"` is never picked implicitly. |
|
|
155
|
+
| `logging.file` | XDG state path | Append-mode log file. `log()` / `logEvent()` tee here in addition to stdout. Defaults to `${XDG_STATE_HOME:-$HOME/.local/state}/groundcrew/groundcrew.log`. |
|
|
156
|
+
|
|
157
|
+
</details>
|
|
158
|
+
|
|
159
|
+
### Per-session credentials (`preLaunch` + `preLaunchEnv`)
|
|
160
|
+
|
|
161
|
+
Build secrets shuttle build-time values _into_ setup. `preLaunch` does the opposite for the _agent_ phase: it runs a host-shell snippet **outside** Safehouse/sdx so the minting snippet never sees `NPM_TOKEN` / `BUF_TOKEN`, and its exports still land in the launch shell. Use this when the agent needs a short-lived credential that has to be minted from something the sandbox can't reach (e.g. an engineer CLI session in Keychain), and you don't want any of that source material — build-time or otherwise — in the minting snippet's environment or in the agent's process.
|
|
162
|
+
|
|
163
|
+
The "preLaunch never sees build secrets" contract is enforced differently per runner — same outcome, different mechanism:
|
|
164
|
+
|
|
165
|
+
- **`runner: "safehouse"` (default):** `preLaunch` runs immediately after `cd`, **before** `secrets.env` is sourced into the launch shell. `.groundcrew/setup.sh` then runs inside its own profile-neutral `safehouse-clearance` wrap with `--env-pass=NPM_TOKEN,BUF_TOKEN`; build secrets are `unset` on the host before the agent's Safehouse wrap is exec'd.
|
|
166
|
+
- **`runner: "none"`:** `secrets.env` is sourced first, `.groundcrew/setup.sh` runs on the host, build-secret names are `unset`, **then** `preLaunch` runs against a clean env, then the agent is exec'd.
|
|
167
|
+
|
|
168
|
+
Same security invariant, enforced via source-after-mint ordering under `safehouse` and post-cleanup ordering under `none`. If your snippet depends on a tool installed by `setup.sh`, prefer `runner: "none"` (preLaunch runs after setup) or inline the dependency; under `safehouse`, preLaunch runs before setup.
|
|
169
|
+
|
|
170
|
+
Under the default `safehouse` runner, the agent runs under a sanitized env allowlist — exports from `preLaunch` land in the launch shell but are stripped before reaching the agent unless they're forwarded. `preLaunchEnv` is the supported way to forward them: groundcrew passes the names via `--env-pass=` on the **agent** Safehouse wrap, so you keep the project's egress host allowlist (`clearance-allow-hosts`) without touching `cmd`. The names are scoped to the agent wrap only — the setup wrap and `.groundcrew/setup.sh` never see them, by design (setup runs profile-neutral and never carries the agent's grants).
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
models: {
|
|
174
|
+
definitions: {
|
|
175
|
+
claude: {
|
|
176
|
+
preLaunch: "SESSION_TOKEN=$(your-mint-command) && export SESSION_TOKEN",
|
|
177
|
+
preLaunchEnv: ["SESSION_TOKEN"],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`&&` ensures `export` only runs when the mint succeeded; a failed mint propagates non-zero out of `preLaunch` and aborts launch before the agent starts. `{{worktree}}` is substituted the same way as in `cmd`. Under `runner: "none"`, exports flow through unchanged and `preLaunchEnv` is a no-op. A **non-empty** `preLaunchEnv` is not supported when `local.runner` resolves to `sdx` in v1 (sdx does not forward arbitrary host env into the sandbox); validated early in `setupWorkspace`. An empty `preLaunchEnv: []` is a uniform no-op in every runner — it forwards zero names, so the unsupported-runner guards do not fire.
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary>Manual fallback when <code>cmd</code> brings its own <code>safehouse</code> wrap</summary>
|
|
187
|
+
|
|
188
|
+
If your `cmd` already starts with `safehouse` (because you need wrap flags groundcrew doesn't provide), groundcrew won't auto-compose `--env-pass=` for you and a **non-empty** `preLaunchEnv` is rejected at launch (an empty `[]` is accepted as a no-op). Add the names to your own `cmd` instead — note that this opts the model out of groundcrew's default `safehouse-clearance` wrap (egress allowlist), so re-supply `--append-profile` / `--env` yourself if you need it:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
claude: {
|
|
192
|
+
preLaunch: "SESSION_TOKEN=$(your-mint-command) && export SESSION_TOKEN",
|
|
193
|
+
cmd: 'safehouse --env-pass=SESSION_TOKEN your-agent-cli',
|
|
194
|
+
},
|
|
195
|
+
```
|
|
154
196
|
|
|
155
197
|
</details>
|
|
156
198
|
|
|
@@ -460,7 +502,7 @@ If a `models.definitions.<name>.cmd` already starts with `safehouse`, groundcrew
|
|
|
460
502
|
<details>
|
|
461
503
|
<summary>Dead tmux windows vanish by default</summary>
|
|
462
504
|
|
|
463
|
-
When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible.
|
|
505
|
+
When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm install` crash), the tmux window closes immediately and the error scrolls into the void. Set `GROUNDCREW_KEEP_DEAD_WINDOWS=1` in the env you launch `crew` from to flip the per-window `remain-on-exit` to `on`; the window stays open with the error visible. `crew status` reports those kept windows as `exited` and keeps the tmux attach command visible so you can inspect scrollback before resuming or cleaning up. tmux backend only.
|
|
464
506
|
|
|
465
507
|
</details>
|
|
466
508
|
|
package/crew.config.example.ts
CHANGED
|
@@ -61,6 +61,16 @@ export default {
|
|
|
61
61
|
// cmd: "cursor-agent",
|
|
62
62
|
// color: "#929292",
|
|
63
63
|
// },
|
|
64
|
+
// // Optional: mint a short-lived credential outside Safehouse and
|
|
65
|
+
// // forward it into the agent. `preLaunch` runs in the launch shell
|
|
66
|
+
// // before the agent exec; `preLaunchEnv` lists the names to add to
|
|
67
|
+
// // groundcrew's `safehouse-clearance --env-pass=` flag so the wrap's
|
|
68
|
+
// // egress allowlist stays intact. Chain with `&&` so a failed mint
|
|
69
|
+
// // aborts launch before `export`.
|
|
70
|
+
// // claude: {
|
|
71
|
+
// // preLaunch: "SESSION_TOKEN=$(your-mint-command) && export SESSION_TOKEN",
|
|
72
|
+
// // preLaunchEnv: ["SESSION_TOKEN"],
|
|
73
|
+
// // },
|
|
64
74
|
// // To run a model under the sdx (Docker Sandboxes) runner, bind it to
|
|
65
75
|
// // an sbx agent. Required when `local.runner` resolves to `sdx`.
|
|
66
76
|
// // claude: { sandbox: { agent: "claude" } },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAanE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkjBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
|
package/dist/commands/status.js
CHANGED
|
@@ -68,8 +68,14 @@ function ticketWorkspaceText(probe, ticket) {
|
|
|
68
68
|
if (probe.kind === "unavailable") {
|
|
69
69
|
return workspaceProbeUnavailableLine(probe);
|
|
70
70
|
}
|
|
71
|
+
if (isWorkspaceExited(probe, ticket)) {
|
|
72
|
+
return "exited";
|
|
73
|
+
}
|
|
71
74
|
return probe.names.has(ticket) ? "live" : "not live";
|
|
72
75
|
}
|
|
76
|
+
function isWorkspaceExited(probe, ticket) {
|
|
77
|
+
return probe.kind === "ok" && probe.exitedNames?.has(ticket) === true;
|
|
78
|
+
}
|
|
73
79
|
function formatRunState(state) {
|
|
74
80
|
if (state === undefined) {
|
|
75
81
|
return "(none)";
|
|
@@ -116,6 +122,17 @@ function writeRecentLogs(config, ticket) {
|
|
|
116
122
|
writeSection("Recent logs");
|
|
117
123
|
writeOutput(logLines.join("\n"));
|
|
118
124
|
}
|
|
125
|
+
async function exitedWorkspaceAccessHint(config, probe, ticket) {
|
|
126
|
+
if (!isWorkspaceExited(probe, ticket)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await withLogOutputSuppressed(async () => await workspaces.accessHint(config, ticket));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
119
136
|
function formatTicketLine(ticket, runState, sourceStatus) {
|
|
120
137
|
const parts = [`ticket: ${ticket}`];
|
|
121
138
|
if (sourceStatus.kind === "found") {
|
|
@@ -154,10 +171,14 @@ async function writeTicketStatus(config, rawTicket) {
|
|
|
154
171
|
withLogOutputSuppressed(async () => await workspaces.probe(config)),
|
|
155
172
|
readTicketSourceStatus(config, ticket),
|
|
156
173
|
]);
|
|
174
|
+
const accessHint = await exitedWorkspaceAccessHint(config, workspaceProbe, ticket);
|
|
157
175
|
writeOutput(formatTicketLine(ticket, runState, sourceStatus));
|
|
158
176
|
writeTicketTitle(runState, sourceStatus);
|
|
159
177
|
writeOutput(`run: ${formatRunState(runState)}`);
|
|
160
178
|
writeOutput(`workspace: ${ticketWorkspaceText(workspaceProbe, ticket)}`);
|
|
179
|
+
if (accessHint !== undefined) {
|
|
180
|
+
writeOutput(`attach: ${accessHint.command}`);
|
|
181
|
+
}
|
|
161
182
|
await writeTicketWorktrees(config, ticket);
|
|
162
183
|
writeRecentLogs(config, ticket);
|
|
163
184
|
}
|
|
@@ -202,22 +223,26 @@ function formatDuration(ms) {
|
|
|
202
223
|
/**
|
|
203
224
|
* Combined human-readable state for the inventory row. Surfaces RunState
|
|
204
225
|
* lifecycle and flags the two interesting disagreements with the workspace
|
|
205
|
-
* probe
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
226
|
+
* probe. A recorded running dispatch can have a missing or exited session;
|
|
227
|
+
* an idle row can have a stray live or exited session. `probe.kind ===
|
|
228
|
+
* "unavailable"` is treated as "we don't know" and never produces a suffix.
|
|
229
|
+
* When the row is actively running, appends the elapsed wall-clock time since
|
|
230
|
+
* dispatch.
|
|
210
231
|
*/
|
|
211
232
|
function inventoryStateText(runState, probe, ticket, now) {
|
|
212
233
|
const lifecycle = runState?.state ?? "idle";
|
|
213
234
|
const duration = runStateDurationMs(runState, now);
|
|
214
235
|
const flags = [];
|
|
215
236
|
if (probe.kind === "ok") {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
237
|
+
const sessionPresent = probe.names.has(ticket);
|
|
238
|
+
const sessionExited = isWorkspaceExited(probe, ticket);
|
|
239
|
+
if (lifecycle === "idle" && sessionPresent) {
|
|
240
|
+
flags.push(sessionExited ? "stray exited session" : "stray session");
|
|
241
|
+
}
|
|
242
|
+
if ((lifecycle === "running" || lifecycle === "resumed") && sessionExited) {
|
|
243
|
+
flags.push("session exited");
|
|
219
244
|
}
|
|
220
|
-
if ((lifecycle === "running" || lifecycle === "resumed") && !
|
|
245
|
+
else if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
|
|
221
246
|
flags.push("session dead");
|
|
222
247
|
}
|
|
223
248
|
}
|
|
@@ -231,9 +256,11 @@ function inventoryStateText(runState, probe, ticket, now) {
|
|
|
231
256
|
* probe disagree. Returned commands are safe defaults; the user is free to
|
|
232
257
|
* ignore them and use `attach:` + `pr:` to investigate first.
|
|
233
258
|
*
|
|
234
|
-
* - Stray session (
|
|
235
|
-
* tear down the orphaned worktree
|
|
236
|
-
* - Session
|
|
259
|
+
* - Stray session (session present, no run-state record) -> `crew cleanup`
|
|
260
|
+
* to tear down the orphaned worktree and close the session.
|
|
261
|
+
* - Session exited (run-state says running/resumed, kept dead tmux window)
|
|
262
|
+
* -> attach first so the failed command remains available for inspection.
|
|
263
|
+
* - Session dead (run-state says running/resumed, no session present) ->
|
|
237
264
|
* `crew resume` to bring the agent back; the worktree is preserved.
|
|
238
265
|
*
|
|
239
266
|
* No hint when the probe is unavailable (we genuinely don't know whether
|
|
@@ -244,11 +271,17 @@ function inventoryHint(runState, probe, ticket) {
|
|
|
244
271
|
return undefined;
|
|
245
272
|
}
|
|
246
273
|
const lifecycle = runState?.state ?? "idle";
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
274
|
+
const sessionPresent = probe.names.has(ticket);
|
|
275
|
+
const sessionExited = isWorkspaceExited(probe, ticket);
|
|
276
|
+
if (lifecycle === "idle" && sessionPresent) {
|
|
277
|
+
return sessionExited
|
|
278
|
+
? `run 'crew cleanup ${ticket}' to clear this stray exited session`
|
|
279
|
+
: `run 'crew cleanup ${ticket}' to clear this stray session`;
|
|
280
|
+
}
|
|
281
|
+
if ((lifecycle === "running" || lifecycle === "resumed") && sessionExited) {
|
|
282
|
+
return `attach to inspect scrollback, then run 'crew resume ${ticket}'`;
|
|
250
283
|
}
|
|
251
|
-
if ((lifecycle === "running" || lifecycle === "resumed") && !
|
|
284
|
+
if ((lifecycle === "running" || lifecycle === "resumed") && !sessionPresent) {
|
|
252
285
|
return `run 'crew resume ${ticket}' to bring the session back`;
|
|
253
286
|
}
|
|
254
287
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsD/B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
|
package/dist/lib/agentLaunch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureClearance } from "@clipboard-health/clearance";
|
|
2
|
+
import { hasPreLaunchEnv, } from "./config.js";
|
|
2
3
|
import { detectHostCapabilities } from "./host.js";
|
|
3
4
|
import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
|
|
4
5
|
import { sandboxNameFor } from "./sandboxName.js";
|
|
@@ -25,6 +26,23 @@ export async function prepareAgentLaunch(input) {
|
|
|
25
26
|
if (runner === "sdx" && input.definition.sandbox === undefined) {
|
|
26
27
|
throw new Error(`Local groundcrew ${input.purpose} with the sdx runner require a sandbox config on model '${input.model}'.`);
|
|
27
28
|
}
|
|
29
|
+
if (runner === "sdx" && input.definition.preLaunch !== undefined) {
|
|
30
|
+
throw new Error(`Local groundcrew ${input.purpose} with the sdx runner do not support preLaunch on model '${input.model}'. ` +
|
|
31
|
+
"Use local.runner 'safehouse' or 'none', or remove preLaunch from the model.");
|
|
32
|
+
}
|
|
33
|
+
if (runner === "sdx" && hasPreLaunchEnv(input.definition)) {
|
|
34
|
+
throw new Error(`Local groundcrew ${input.purpose} with the sdx runner do not support preLaunchEnv on model '${input.model}'. ` +
|
|
35
|
+
"Use local.runner 'safehouse' or 'none', or remove preLaunchEnv from the model.");
|
|
36
|
+
}
|
|
37
|
+
// Mirror of buildLaunchCommand's defense — fail at config-resolution time so
|
|
38
|
+
// the operator sees the problem before the workspace is spawned, not deep in
|
|
39
|
+
// the launch shell. The buildLaunchCommand check stays as defense in depth.
|
|
40
|
+
if (runner === "safehouse" &&
|
|
41
|
+
hasPreLaunchEnv(input.definition) &&
|
|
42
|
+
/^safehouse(\s|$)/.test(input.definition.cmd)) {
|
|
43
|
+
throw new Error(`Local groundcrew ${input.purpose} on model '${input.model}' cannot inject preLaunchEnv when 'cmd' already starts with 'safehouse'. ` +
|
|
44
|
+
"Your cmd owns the wrap, so add the names to its own '--env-pass=' flag, or drop the 'safehouse' prefix from 'cmd' to let groundcrew compose the flag for you.");
|
|
45
|
+
}
|
|
28
46
|
const sandboxName = runner === "sdx" && input.definition.sandbox !== undefined
|
|
29
47
|
? sandboxNameFor({ agent: input.definition.sandbox.agent })
|
|
30
48
|
: undefined;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -64,6 +64,30 @@ export interface ModelDefinition {
|
|
|
64
64
|
* Groundcrew adds the Safehouse wrapper.
|
|
65
65
|
*/
|
|
66
66
|
cmd: string;
|
|
67
|
+
/**
|
|
68
|
+
* Optional shell snippet run in the launch shell **before** the agent is
|
|
69
|
+
* exec'd and **outside** Safehouse/sdx. Use to mint short-lived credentials
|
|
70
|
+
* (e.g. `export SESSION_TOKEN=...`) that the wrapped `cmd` inherits via
|
|
71
|
+
* the process environment. `{{worktree}}` is replaced before launch.
|
|
72
|
+
* Failures abort launch (unlike deps setup, which logs and continues).
|
|
73
|
+
* Not supported for `local.runner` `sdx` in v1.
|
|
74
|
+
*/
|
|
75
|
+
preLaunch?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional list of env var names to forward from the launch shell into
|
|
78
|
+
* the agent under the safehouse runner. Companion to `preLaunch` —
|
|
79
|
+
* names exported by `preLaunch` go here so groundcrew appends them to
|
|
80
|
+
* the `safehouse-clearance` wrap's `--env-pass=` flag, preserving the
|
|
81
|
+
* project's egress allowlist (`clearance-allow-hosts`) without forcing
|
|
82
|
+
* the user to rewrite `cmd`. Under `local.runner: "none"` exports flow
|
|
83
|
+
* through unchanged, so `preLaunchEnv` is a no-op. An empty array is a
|
|
84
|
+
* uniform no-op in every runner (it forwards zero names, so the
|
|
85
|
+
* unsupported-runner guards do not fire). A non-empty list is rejected
|
|
86
|
+
* when `local.runner` resolves to `sdx` in v1, and when `cmd` already
|
|
87
|
+
* starts with `safehouse` (the user owns env forwarding in that case).
|
|
88
|
+
* Each name must match `[A-Za-z_][A-Za-z0-9_]*` (POSIX env var name).
|
|
89
|
+
*/
|
|
90
|
+
preLaunchEnv?: string[];
|
|
67
91
|
color: string;
|
|
68
92
|
usage?: {
|
|
69
93
|
codexbar: {
|
|
@@ -219,6 +243,17 @@ export interface ResolvedConfig {
|
|
|
219
243
|
file: string;
|
|
220
244
|
};
|
|
221
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Single source of truth for "is preLaunchEnv asking us to forward anything?"
|
|
248
|
+
*
|
|
249
|
+
* An empty array forwards zero names, so it is a uniform no-op in every
|
|
250
|
+
* runner. The unsupported-runner guards (sdx, safehouse-prefixed cmd) only
|
|
251
|
+
* fire when there is actually something to forward — rejecting `[]` only on
|
|
252
|
+
* those runners would make an empty list accepted under `safehouse`/`none`
|
|
253
|
+
* but fatal elsewhere, which is a worse asymmetry than what the helper
|
|
254
|
+
* collapses. Centralized so all four call sites stay in lockstep.
|
|
255
|
+
*/
|
|
256
|
+
export declare function hasPreLaunchEnv(definition: Pick<ModelDefinition, "preLaunchEnv">): boolean;
|
|
222
257
|
/**
|
|
223
258
|
* True when `name` is a shipped default the user removed via `disabled: true`.
|
|
224
259
|
* Derived from absence in `definitions` — that's the only path that removes a
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import { cosmiconfig } from "cosmiconfig";
|
|
7
7
|
import { log, readEnvironmentVariable, setLogFile } from "./util.js";
|
|
8
8
|
import { xdgConfigPath, xdgStatePath } from "./xdg.js";
|
|
9
|
+
import { BUILD_SECRET_NAMES } from "./buildSecrets.js";
|
|
9
10
|
export { BUILD_SECRET_NAMES } from "./buildSecrets.js";
|
|
10
11
|
/**
|
|
11
12
|
* Reserved model name. A ticket labeled `agent-any` resolves at runtime
|
|
@@ -127,6 +128,40 @@ function normalizeOptionalString(value, path) {
|
|
|
127
128
|
}
|
|
128
129
|
return value.trim();
|
|
129
130
|
}
|
|
131
|
+
const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
132
|
+
function validatePreLaunchEnv(modelName, value) {
|
|
133
|
+
const path = `models.definitions.${modelName}.preLaunchEnv`;
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
fail(`${path} must be an array of env var names (got ${JSON.stringify(value)})`);
|
|
136
|
+
}
|
|
137
|
+
for (const [index, entry] of value.entries()) {
|
|
138
|
+
if (typeof entry !== "string" || !ENV_VAR_NAME_PATTERN.test(entry)) {
|
|
139
|
+
fail(`${path}[${index}] must be a POSIX env var name matching ${ENV_VAR_NAME_PATTERN.source} (got ${JSON.stringify(entry)})`);
|
|
140
|
+
}
|
|
141
|
+
// Build secrets are sourced into the host launch shell, forwarded only to
|
|
142
|
+
// the Safehouse *setup* wrap, and `unset` on the host before the agent
|
|
143
|
+
// wrap is exec'd. Listing one here would silently never reach the agent —
|
|
144
|
+
// fail loudly so the operator picks a different name (or removes the
|
|
145
|
+
// entry) instead of debugging a missing env var at runtime.
|
|
146
|
+
if (BUILD_SECRET_NAMES.includes(entry)) {
|
|
147
|
+
fail(`${path}[${index}] cannot be a BUILD_SECRET_NAMES entry (${BUILD_SECRET_NAMES.join(", ")}); ` +
|
|
148
|
+
"those are unset on the host before the agent wrap is exec'd, so forwarding them via --env-pass would be a no-op.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Single source of truth for "is preLaunchEnv asking us to forward anything?"
|
|
154
|
+
*
|
|
155
|
+
* An empty array forwards zero names, so it is a uniform no-op in every
|
|
156
|
+
* runner. The unsupported-runner guards (sdx, safehouse-prefixed cmd) only
|
|
157
|
+
* fire when there is actually something to forward — rejecting `[]` only on
|
|
158
|
+
* those runners would make an empty list accepted under `safehouse`/`none`
|
|
159
|
+
* but fatal elsewhere, which is a worse asymmetry than what the helper
|
|
160
|
+
* collapses. Centralized so all four call sites stay in lockstep.
|
|
161
|
+
*/
|
|
162
|
+
export function hasPreLaunchEnv(definition) {
|
|
163
|
+
return definition.preLaunchEnv !== undefined && definition.preLaunchEnv.length > 0;
|
|
164
|
+
}
|
|
130
165
|
function isWorkspaceKindSetting(value) {
|
|
131
166
|
return (typeof value === "string" && WORKSPACE_KIND_SETTINGS.includes(value));
|
|
132
167
|
}
|
|
@@ -189,7 +224,7 @@ function failIfLegacyModelKeys(name, override) {
|
|
|
189
224
|
if (override["disabled"] !== true) {
|
|
190
225
|
fail(`models.definitions.${name}.disabled must be exactly \`true\` when set (got ${JSON.stringify(override["disabled"])})`);
|
|
191
226
|
}
|
|
192
|
-
const conflicting = ["cmd", "color", "usage", "sandbox"].filter((key) => Object.hasOwn(override, key));
|
|
227
|
+
const conflicting = ["cmd", "color", "usage", "sandbox", "preLaunch", "preLaunchEnv"].filter((key) => Object.hasOwn(override, key));
|
|
193
228
|
if (conflicting.length > 0) {
|
|
194
229
|
fail(`models.definitions.${name}: cannot combine \`disabled: true\` with other fields (${conflicting.join(", ")}). Either disable the model or override its fields, not both.`);
|
|
195
230
|
}
|
|
@@ -208,6 +243,36 @@ export function isShippedDefaultDisabled(config, name) {
|
|
|
208
243
|
function isUsageDisableSentinel(usage) {
|
|
209
244
|
return isPlainObject(usage) && "disabled" in usage && usage.disabled;
|
|
210
245
|
}
|
|
246
|
+
function buildOverrideCandidate(name, override, existing) {
|
|
247
|
+
const base = existing === undefined ? {} : cloneModelDefinition(existing);
|
|
248
|
+
// Per-key spread so overriding `cmd` alone preserves the default
|
|
249
|
+
// `color` / `usage`. Brand-new entries must supply both required fields.
|
|
250
|
+
const candidate = { ...base };
|
|
251
|
+
if (override.cmd !== undefined) {
|
|
252
|
+
candidate.cmd = override.cmd;
|
|
253
|
+
}
|
|
254
|
+
if (override.color !== undefined) {
|
|
255
|
+
candidate.color = override.color;
|
|
256
|
+
}
|
|
257
|
+
if (override.usage !== undefined) {
|
|
258
|
+
if (isUsageDisableSentinel(override.usage)) {
|
|
259
|
+
delete candidate.usage;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
candidate.usage = override.usage;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (override.sandbox !== undefined) {
|
|
266
|
+
candidate.sandbox = normalizeSandbox(override.sandbox, `models.definitions.${name}.sandbox`);
|
|
267
|
+
}
|
|
268
|
+
if (override.preLaunch !== undefined) {
|
|
269
|
+
candidate.preLaunch = override.preLaunch;
|
|
270
|
+
}
|
|
271
|
+
if (override.preLaunchEnv !== undefined) {
|
|
272
|
+
candidate.preLaunchEnv = override.preLaunchEnv;
|
|
273
|
+
}
|
|
274
|
+
return candidate;
|
|
275
|
+
}
|
|
211
276
|
function mergeDefinitions(user) {
|
|
212
277
|
if (user !== undefined && !isPlainObject(user)) {
|
|
213
278
|
fail("models.definitions must be an object");
|
|
@@ -229,28 +294,8 @@ function mergeDefinitions(user) {
|
|
|
229
294
|
delete merged[name];
|
|
230
295
|
continue;
|
|
231
296
|
}
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
// `color` / `usage`. Brand-new entries must supply both required fields.
|
|
235
|
-
const candidate = { ...base };
|
|
236
|
-
if (override.cmd !== undefined) {
|
|
237
|
-
candidate.cmd = override.cmd;
|
|
238
|
-
}
|
|
239
|
-
if (override.color !== undefined) {
|
|
240
|
-
candidate.color = override.color;
|
|
241
|
-
}
|
|
242
|
-
if (override.usage !== undefined) {
|
|
243
|
-
if (isUsageDisableSentinel(override.usage)) {
|
|
244
|
-
delete candidate.usage;
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
candidate.usage = override.usage;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (override.sandbox !== undefined) {
|
|
251
|
-
candidate.sandbox = normalizeSandbox(override.sandbox, `models.definitions.${name}.sandbox`);
|
|
252
|
-
}
|
|
253
|
-
const { cmd, color, usage, sandbox } = candidate;
|
|
297
|
+
const candidate = buildOverrideCandidate(name, override, merged[name]);
|
|
298
|
+
const { cmd, color, usage, sandbox, preLaunch, preLaunchEnv } = candidate;
|
|
254
299
|
if (typeof cmd !== "string" || cmd.length === 0) {
|
|
255
300
|
fail(`models.definitions.${name}.cmd must be a non-empty string`);
|
|
256
301
|
}
|
|
@@ -264,6 +309,12 @@ function mergeDefinitions(user) {
|
|
|
264
309
|
if (sandbox !== undefined) {
|
|
265
310
|
definition.sandbox = sandbox;
|
|
266
311
|
}
|
|
312
|
+
if (preLaunch !== undefined) {
|
|
313
|
+
definition.preLaunch = preLaunch;
|
|
314
|
+
}
|
|
315
|
+
if (preLaunchEnv !== undefined) {
|
|
316
|
+
definition.preLaunchEnv = preLaunchEnv;
|
|
317
|
+
}
|
|
267
318
|
merged[name] = definition;
|
|
268
319
|
}
|
|
269
320
|
return merged;
|
|
@@ -431,6 +482,15 @@ function validate(config) {
|
|
|
431
482
|
if (definition.sandbox !== undefined) {
|
|
432
483
|
requireString(definition.sandbox.agent, `models.definitions.${name}.sandbox.agent`);
|
|
433
484
|
}
|
|
485
|
+
if (definition.preLaunch !== undefined) {
|
|
486
|
+
requireString(definition.preLaunch, `models.definitions.${name}.preLaunch`);
|
|
487
|
+
if (definition.preLaunch.trim().length === 0) {
|
|
488
|
+
fail(`models.definitions.${name}.preLaunch must contain non-whitespace characters`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (definition.preLaunchEnv !== undefined) {
|
|
492
|
+
validatePreLaunchEnv(name, definition.preLaunchEnv);
|
|
493
|
+
}
|
|
434
494
|
}
|
|
435
495
|
/* v8 ignore next 5 @preserve -- normalizeLocalRunner rejects invalid strings before validate() runs; this is a belt-and-suspenders guard */
|
|
436
496
|
if (!LOCAL_RUNNER_SETTINGS.includes(config.local.runner)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;GAGG;AACH,eAAO,MAAM,aAAa,mFACwD,CAAC;AAyKnF,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CA0B7E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { basename, dirname, resolve } from "node:path";
|
|
3
|
-
import { BUILD_SECRET_NAMES } from "./config.js";
|
|
3
|
+
import { BUILD_SECRET_NAMES, hasPreLaunchEnv, } from "./config.js";
|
|
4
4
|
import { shellSingleQuote } from "./shell.js";
|
|
5
5
|
export { shellSingleQuote } from "./shell.js";
|
|
6
6
|
/**
|
|
@@ -35,6 +35,9 @@ function renderAgentCommand(arguments_) {
|
|
|
35
35
|
.replaceAll("{{worktree}}", shellSingleQuote(arguments_.worktreeDir))
|
|
36
36
|
.replaceAll("{{sandbox}}", shellSingleQuote(arguments_.sandboxName));
|
|
37
37
|
}
|
|
38
|
+
function renderPreLaunch(preLaunch, worktreeDir) {
|
|
39
|
+
return preLaunch.replaceAll("{{worktree}}", shellSingleQuote(worktreeDir));
|
|
40
|
+
}
|
|
38
41
|
function setupWithStatusReporting(setupCommand) {
|
|
39
42
|
return [
|
|
40
43
|
setupCommand,
|
|
@@ -50,8 +53,45 @@ function setupWithStatusReporting(setupCommand) {
|
|
|
50
53
|
function sourceSecretsLine(secretsFile) {
|
|
51
54
|
return `if [ -f ${shellSingleQuote(secretsFile)} ]; then set -a && . ${shellSingleQuote(secretsFile)} && set +a; fi`;
|
|
52
55
|
}
|
|
56
|
+
function unsetEnvironmentLine(names) {
|
|
57
|
+
return `unset ${[...new Set(names)].join(" ")}`;
|
|
58
|
+
}
|
|
53
59
|
function unsetSecretsLine() {
|
|
54
|
-
return
|
|
60
|
+
return unsetEnvironmentLine(BUILD_SECRET_NAMES);
|
|
61
|
+
}
|
|
62
|
+
function trapCleanupLine(promptDir) {
|
|
63
|
+
const cleanupCmd = `rm -rf ${shellSingleQuote(promptDir)}`;
|
|
64
|
+
return `trap ${shellSingleQuote(cleanupCmd)} EXIT`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Shared head of every host-shell `&&` chain: arm the `EXIT` trap that wipes
|
|
68
|
+
* `promptDir` (must come before any link that can fail, including the `cd`),
|
|
69
|
+
* then `cd` into the worktree. Kept separate from secret sourcing so the
|
|
70
|
+
* safehouse path can splice `preLaunch` between the `cd` and the secrets
|
|
71
|
+
* source — preLaunch must never see build-time secrets in env.
|
|
72
|
+
*/
|
|
73
|
+
function hostTrapAndCd(arguments_) {
|
|
74
|
+
return [trapCleanupLine(arguments_.promptDir), `cd ${shellSingleQuote(arguments_.worktreeDir)}`];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Optional source-of-secrets line. Returns `[]` when no `secretsFile` is
|
|
78
|
+
* staged so callers can splat the result into their chain unconditionally.
|
|
79
|
+
*/
|
|
80
|
+
function hostSourceSecrets(secretsFile) {
|
|
81
|
+
return secretsFile === undefined ? [] : [sourceSecretsLine(secretsFile)];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Shared tail of every host-shell `&&` chain: optional `preLaunch`, then the
|
|
85
|
+
* staged prompt read, the explicit success-path `rm -rf` (the trap covers the
|
|
86
|
+
* failure path), and the final `exec` of whatever wraps (or is) the agent.
|
|
87
|
+
*/
|
|
88
|
+
function preLaunchPromptAndExec(arguments_) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
if (arguments_.definition.preLaunch !== undefined) {
|
|
91
|
+
lines.push(renderPreLaunch(arguments_.definition.preLaunch, arguments_.worktreeDir));
|
|
92
|
+
}
|
|
93
|
+
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(arguments_.promptDir)}`, arguments_.execLine);
|
|
94
|
+
return lines;
|
|
55
95
|
}
|
|
56
96
|
function tokenizeShellPrefix(command) {
|
|
57
97
|
const tokens = [];
|
|
@@ -134,11 +174,23 @@ function safehouseProfileCommandName(agentCmd) {
|
|
|
134
174
|
*/
|
|
135
175
|
export function buildLaunchCommand(arguments_) {
|
|
136
176
|
if (arguments_.runner === "sdx") {
|
|
177
|
+
if (arguments_.definition.preLaunch !== undefined) {
|
|
178
|
+
throw new Error("preLaunch is not yet supported for runner='sdx'. Set local.runner to 'safehouse' or 'none', or open an issue for sdx support.");
|
|
179
|
+
}
|
|
180
|
+
if (hasPreLaunchEnv(arguments_.definition)) {
|
|
181
|
+
throw new Error("preLaunchEnv is not yet supported for runner='sdx'. Set local.runner to 'safehouse' or 'none', or open an issue for sdx support.");
|
|
182
|
+
}
|
|
137
183
|
return buildSdxLaunchCommand(arguments_);
|
|
138
184
|
}
|
|
139
185
|
if (shouldWrapWithSafehouse(arguments_)) {
|
|
140
186
|
return buildSafehouseLaunchCommand(arguments_);
|
|
141
187
|
}
|
|
188
|
+
if (hasPreLaunchEnv(arguments_.definition) && arguments_.runner === "safehouse") {
|
|
189
|
+
// `runner === "safehouse"` but `cmd` already starts with `safehouse` — the
|
|
190
|
+
// user owns env forwarding in that case, so there's no wrap flag for us to
|
|
191
|
+
// inject into. Fail loudly instead of silently dropping the contract.
|
|
192
|
+
throw new Error("preLaunchEnv cannot be injected when `cmd` starts with `safehouse` — your cmd owns the wrap, so add the names to its own `--env-pass=` flag, or drop the `safehouse` prefix from `cmd` to let groundcrew compose the flag for you.");
|
|
193
|
+
}
|
|
142
194
|
return buildUnwrappedHostLaunchCommand(arguments_);
|
|
143
195
|
}
|
|
144
196
|
/**
|
|
@@ -165,30 +217,50 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
165
217
|
worktreeDir: arguments_.worktreeDir,
|
|
166
218
|
sandboxName: "",
|
|
167
219
|
});
|
|
168
|
-
const lines = [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
const lines = [
|
|
221
|
+
...hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir }),
|
|
222
|
+
...hostSourceSecrets(arguments_.secretsFile),
|
|
223
|
+
setupWithStatusReporting(SETUP_COMMAND),
|
|
224
|
+
];
|
|
173
225
|
if (arguments_.secretsFile !== undefined) {
|
|
174
226
|
lines.push(unsetSecretsLine());
|
|
175
227
|
}
|
|
176
|
-
lines.push(
|
|
228
|
+
lines.push(...preLaunchPromptAndExec({
|
|
229
|
+
definition: arguments_.definition,
|
|
230
|
+
worktreeDir: arguments_.worktreeDir,
|
|
231
|
+
promptFile: arguments_.promptFile,
|
|
232
|
+
promptDir,
|
|
233
|
+
execLine: `exec ${agentCmd} "$_p"`,
|
|
234
|
+
}));
|
|
177
235
|
return lines.join(" && ");
|
|
178
236
|
}
|
|
179
237
|
/**
|
|
180
|
-
* Safehouse launch.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
238
|
+
* Safehouse launch. Two Safehouse wraps, by design:
|
|
239
|
+
*
|
|
240
|
+
* 1. **Setup wrap**: plain `safehouse-clearance ... sh -c '<setup>'`. Runs
|
|
241
|
+
* `.groundcrew/setup.sh --deps-only` filesystem-isolated and
|
|
242
|
+
* egress-restricted, **without** inheriting agent-profile grants.
|
|
243
|
+
* 2. **Agent wrap**: `safehouse-clearance "$shim" -c '<exec agent>' sh "$_p"`
|
|
244
|
+
* where `$shim` is a `mktemp`-d symlink to `/bin/sh` named after the
|
|
245
|
+
* agent (e.g. `claude`). Safehouse selects the matching agent profile
|
|
246
|
+
* from the wrapped command's basename (`claude-code.sb` etc.) without
|
|
247
|
+
* needing every agent profile enabled globally.
|
|
248
|
+
*
|
|
249
|
+
* Host ordering matters: when a `preLaunch` hook is present, inherited
|
|
250
|
+
* build-secret names and listed `preLaunchEnv` names are cleared before it runs.
|
|
251
|
+
* That keeps the credential-minting snippet from seeing build-time secrets in
|
|
252
|
+
* env — neither inherited values (the launch shell inherits groundcrew's env,
|
|
253
|
+
* from which `stageBuildSecrets` reads them) nor file-sourced values — and keeps
|
|
254
|
+
* stale same-named ambient credentials from being forwarded. `secrets.env` is
|
|
255
|
+
* then sourced into the host launch shell so Safehouse can forward build secrets
|
|
256
|
+
* into the **setup wrap** via `--env-pass=` (Safehouse's `--env=FILE` mode strips
|
|
257
|
+
* them otherwise). After setup returns, `BUILD_SECRET_NAMES` are `unset` again
|
|
258
|
+
* on the host so they cannot reach the agent wrap.
|
|
185
259
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* command-named shim so Safehouse can select the intended agent profile while
|
|
191
|
-
* the actual agent command remains `sh -c`.
|
|
260
|
+
* `--env-pass` composition is split per wrap (deliberate, post PR #128):
|
|
261
|
+
* - Setup wrap forwards build secrets only.
|
|
262
|
+
* - Agent wrap forwards `preLaunchEnv` names only. preLaunch credentials never
|
|
263
|
+
* reach the profile-neutral setup phase.
|
|
192
264
|
*/
|
|
193
265
|
function buildSafehouseLaunchCommand(arguments_) {
|
|
194
266
|
const promptDir = dirname(arguments_.promptFile);
|
|
@@ -200,23 +272,42 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
200
272
|
});
|
|
201
273
|
const setupCommand = setupWithStatusReporting(SETUP_COMMAND);
|
|
202
274
|
const agentCommand = `exec ${agentCmd} "$@"`;
|
|
203
|
-
//
|
|
204
|
-
|
|
275
|
+
// Split --env-pass per wrap: the setup wrap only needs build secrets (so
|
|
276
|
+
// `npm install` etc. can authenticate); the agent wrap only needs the
|
|
277
|
+
// user's preLaunchEnv (build secrets are `unset` on the host between the
|
|
278
|
+
// two wraps, so forwarding them here would silently no-op). Keeps preLaunch
|
|
279
|
+
// credentials out of the profile-neutral setup phase — see PR #128.
|
|
280
|
+
// Trailing space keeps each flag separated from the next argv token.
|
|
281
|
+
const setupEnvPassFlag = arguments_.secretsFile === undefined ? "" : `--env-pass=${BUILD_SECRET_NAMES.join(",")} `;
|
|
282
|
+
const preLaunchEnvNames = arguments_.definition.preLaunchEnv ?? [];
|
|
283
|
+
const agentEnvPassFlag = preLaunchEnvNames.length === 0 ? "" : `--env-pass=${preLaunchEnvNames.join(",")} `;
|
|
205
284
|
const safehouseWrapper = shellSingleQuote(SAFEHOUSE_CLEARANCE_WRAPPER_PATH);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
285
|
+
// Defensive shim+promptDir trap: by the time we arm it, `rm -rf <promptDir>`
|
|
286
|
+
// has already run (line below) so the promptDir wipe is a no-op on the happy
|
|
287
|
+
// path. Keeps the failure-window between shim creation and the explicit
|
|
288
|
+
// post-wrap cleanup covered for both targets without an unarmed window.
|
|
289
|
+
const shimAndPromptCleanup = `rm -rf "$_safehouse_shim_dir"; rm -rf ${shellSingleQuote(promptDir)}`;
|
|
290
|
+
const shimAndPromptTrap = `trap ${shellSingleQuote(shimAndPromptCleanup)} EXIT`;
|
|
291
|
+
const lines = hostTrapAndCd({ worktreeDir: arguments_.worktreeDir, promptDir });
|
|
292
|
+
if (arguments_.definition.preLaunch !== undefined) {
|
|
293
|
+
// Scrub inherited env before preLaunch runs. `stageBuildSecrets` copies
|
|
294
|
+
// build secrets out of `process.env`, and the launch shell inherits that
|
|
295
|
+
// env, so source-after-preLaunch is not enough by itself. Clearing
|
|
296
|
+
// preLaunchEnv names here also prevents stale same-named ambient values
|
|
297
|
+
// from being forwarded if the hook forgets to overwrite them.
|
|
298
|
+
lines.push(unsetEnvironmentLine([...BUILD_SECRET_NAMES, ...preLaunchEnvNames]));
|
|
299
|
+
lines.push(renderPreLaunch(arguments_.definition.preLaunch, arguments_.worktreeDir));
|
|
209
300
|
}
|
|
210
|
-
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${
|
|
301
|
+
lines.push(...hostSourceSecrets(arguments_.secretsFile), `_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${setupEnvPassFlag}sh -c ${shellSingleQuote(setupCommand)}`);
|
|
211
302
|
if (arguments_.secretsFile !== undefined) {
|
|
212
303
|
lines.push(unsetSecretsLine());
|
|
213
304
|
}
|
|
214
|
-
lines.push(`_safehouse_shim_dir=$(mktemp -d "\${TMPDIR:-/tmp}/groundcrew-safehouse-XXXXXX")`,
|
|
305
|
+
lines.push(`_safehouse_shim_dir=$(mktemp -d "\${TMPDIR:-/tmp}/groundcrew-safehouse-XXXXXX")`, shimAndPromptTrap, `_safehouse_shim="$_safehouse_shim_dir/${safehouseCommandName}"`, `ln -s /bin/sh "$_safehouse_shim"`,
|
|
215
306
|
// Safehouse selects an agent profile from the wrapped command's basename.
|
|
216
307
|
// Running the real launch chain as `sh -c` would make it see `sh`, so use
|
|
217
308
|
// an agent-named symlink to /bin/sh. This preserves per-agent profile
|
|
218
309
|
// selection without enabling every agent profile.
|
|
219
|
-
|
|
310
|
+
`{ ${safehouseWrapper} ${agentEnvPassFlag}"$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"; }`);
|
|
220
311
|
return lines.join(" && ");
|
|
221
312
|
}
|
|
222
313
|
function buildSdxLaunchCommand(arguments_) {
|
|
@@ -244,7 +335,7 @@ function buildSdxLaunchCommand(arguments_) {
|
|
|
244
335
|
const sbxEnvironmentFlags = arguments_.secretsFile === undefined
|
|
245
336
|
? ""
|
|
246
337
|
: `${BUILD_SECRET_NAMES.map((name) => `-e ${name}`).join(" ")} `;
|
|
247
|
-
const lines = [];
|
|
338
|
+
const lines = [trapCleanupLine(promptDir)];
|
|
248
339
|
if (arguments_.secretsFile !== undefined) {
|
|
249
340
|
lines.push(sourceSecretsLine(arguments_.secretsFile));
|
|
250
341
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"tmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/tmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAY/B,eAAO,MAAM,WAAW,EAAE,OA+DzB,CAAC"}
|
package/dist/lib/tmuxAdapter.js
CHANGED
|
@@ -17,8 +17,7 @@ export const tmuxAdapter = {
|
|
|
17
17
|
async open(spec, signal) {
|
|
18
18
|
await ensureTmuxSession(signal);
|
|
19
19
|
const target = tmuxTarget(spec.name);
|
|
20
|
-
const
|
|
21
|
-
const keepDeadWindows = keepDeadWindowsEnv !== undefined && keepDeadWindowsEnv.length > 0;
|
|
20
|
+
const keepDeadWindows = shouldKeepDeadWindows();
|
|
22
21
|
await runWorkspaceCommand("tmux", [
|
|
23
22
|
"new-window",
|
|
24
23
|
"-d",
|
|
@@ -54,7 +53,7 @@ export const tmuxAdapter = {
|
|
|
54
53
|
// oxlint-disable-next-line unicorn/no-useless-undefined -- undefined marks the workspace backend as unavailable.
|
|
55
54
|
return undefined;
|
|
56
55
|
}
|
|
57
|
-
return parseTmuxWindows(probe.output);
|
|
56
|
+
return parseTmuxWindows(probe.output, { includeExited: shouldKeepDeadWindows() });
|
|
58
57
|
},
|
|
59
58
|
async close(name, signal) {
|
|
60
59
|
try {
|
|
@@ -78,6 +77,10 @@ export const tmuxAdapter = {
|
|
|
78
77
|
function tmuxTarget(name) {
|
|
79
78
|
return `${TMUX_SESSION}:${name}`;
|
|
80
79
|
}
|
|
80
|
+
function shouldKeepDeadWindows() {
|
|
81
|
+
const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
|
|
82
|
+
return keepDeadWindowsEnv === "1";
|
|
83
|
+
}
|
|
81
84
|
function isTmuxNotFoundError(error) {
|
|
82
85
|
// runCommand surfaces the child's stderr in error.message, so the "no
|
|
83
86
|
// server" / "missing session" / "can't find window" signatures are visible
|
|
@@ -130,7 +133,7 @@ async function ensureTmuxSession(signal) {
|
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
|
-
function parseTmuxWindows(output) {
|
|
136
|
+
function parseTmuxWindows(output, options = {}) {
|
|
134
137
|
const items = [];
|
|
135
138
|
for (const line of output.split("\n")) {
|
|
136
139
|
if (line.length === 0) {
|
|
@@ -147,10 +150,11 @@ function parseTmuxWindows(output) {
|
|
|
147
150
|
// pane_dead != 0 means the command exited and the window is a zombie
|
|
148
151
|
// (only happens when remain-on-exit is on; defense in depth in case a
|
|
149
152
|
// user-globally-set value beats our per-window override).
|
|
150
|
-
|
|
153
|
+
const isExited = deadFlag !== undefined && deadFlag !== "0";
|
|
154
|
+
if (isExited && options.includeExited !== true) {
|
|
151
155
|
continue;
|
|
152
156
|
}
|
|
153
|
-
items.push({ name });
|
|
157
|
+
items.push(isExited ? { name, state: "exited" } : { name });
|
|
154
158
|
}
|
|
155
159
|
return items;
|
|
156
160
|
}
|
|
@@ -10,6 +10,8 @@ export type WorkspaceKind = "cmux" | "tmux";
|
|
|
10
10
|
export interface Workspace {
|
|
11
11
|
/** Ticket id; the join key callers use. */
|
|
12
12
|
name: string;
|
|
13
|
+
/** Omitted means live, for backends that do not expose an exited state. */
|
|
14
|
+
state?: "exited";
|
|
13
15
|
}
|
|
14
16
|
export interface WorkspaceStatus {
|
|
15
17
|
text: string;
|
|
@@ -37,6 +39,7 @@ export interface OpenSpec {
|
|
|
37
39
|
export type WorkspaceProbe = {
|
|
38
40
|
kind: "ok";
|
|
39
41
|
names: Set<string>;
|
|
42
|
+
exitedNames?: Set<string>;
|
|
40
43
|
} | {
|
|
41
44
|
kind: "unavailable";
|
|
42
45
|
error?: unknown;
|
|
@@ -60,7 +63,7 @@ export type WorkspaceCloseResult = {
|
|
|
60
63
|
export interface Adapter {
|
|
61
64
|
open(spec: OpenSpec, signal?: AbortSignal): Promise<void>;
|
|
62
65
|
/**
|
|
63
|
-
*
|
|
66
|
+
* Known workspaces. Returns:
|
|
64
67
|
* - `Workspace[]` when the adapter probe succeeded (may be empty).
|
|
65
68
|
* - `undefined` when the adapter binary failed in a way that doesn't
|
|
66
69
|
* distinguish "no live workspaces" from "couldn't ask".
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,wBAAwB,GAChC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IAC7D,0DAA0D;IAC1D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzE;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC3D;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAE7D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,GAAG,mBAAmB,CAUtF;AAsDD,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,iBAAe,sBAAsB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,iBAAe,kBAAkB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SACD,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC;IAIhC,SAAS;IACT,UAAU;CACX,CAAC"}
|
package/dist/lib/workspaces.js
CHANGED
|
@@ -74,7 +74,9 @@ async function probeWorkspaces(config, signal) {
|
|
|
74
74
|
if (raw === undefined) {
|
|
75
75
|
return { kind: "unavailable" };
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
const names = new Set(raw.map((ws) => ws.name));
|
|
78
|
+
const exitedNames = new Set(raw.filter((ws) => ws.state === "exited").map((ws) => ws.name));
|
|
79
|
+
return exitedNames.size === 0 ? { kind: "ok", names } : { kind: "ok", names, exitedNames };
|
|
78
80
|
}
|
|
79
81
|
async function accessHintForWorkspace(config, name, signal) {
|
|
80
82
|
const adapter = await adapterFor(config, signal);
|
package/package.json
CHANGED