@clipboard-health/groundcrew 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/crew.config.example.ts +5 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +64 -10
- package/dist/commands/interruptWorkspace.d.ts.map +1 -1
- package/dist/commands/interruptWorkspace.js +3 -3
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +1 -2
- package/dist/commands/setupRepos.d.ts.map +1 -1
- package/dist/commands/setupRepos.js +2 -13
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +1 -7
- package/dist/lib/agentLaunch.d.ts +0 -6
- package/dist/lib/agentLaunch.d.ts.map +1 -1
- package/dist/lib/agentLaunch.js +1 -12
- package/dist/lib/cmuxAdapter.d.ts +8 -0
- package/dist/lib/cmuxAdapter.d.ts.map +1 -0
- package/dist/lib/cmuxAdapter.js +163 -0
- package/dist/lib/config.d.ts +2 -76
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +29 -102
- package/dist/lib/launchCommand.d.ts +3 -3
- package/dist/lib/sandboxName.d.ts +9 -0
- package/dist/lib/sandboxName.d.ts.map +1 -0
- package/dist/lib/sandboxName.js +12 -0
- package/dist/lib/tmuxAdapter.d.ts +9 -0
- package/dist/lib/tmuxAdapter.d.ts.map +1 -0
- package/dist/lib/tmuxAdapter.js +156 -0
- package/dist/lib/util.d.ts +11 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +21 -0
- package/dist/lib/workspaceAdapter.d.ts +79 -0
- package/dist/lib/workspaceAdapter.d.ts.map +1 -0
- package/dist/lib/workspaceAdapter.js +17 -0
- package/dist/lib/workspaces.d.ts +7 -55
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +8 -404
- package/package.json +1 -2
- package/dist/commands/sandbox/auth.d.ts +0 -3
- package/dist/commands/sandbox/auth.d.ts.map +0 -1
- package/dist/commands/sandbox/auth.js +0 -227
- package/dist/commands/sandbox/index.d.ts +0 -2
- package/dist/commands/sandbox/index.d.ts.map +0 -1
- package/dist/commands/sandbox/index.js +0 -47
- package/dist/commands/sandbox/inspect.d.ts +0 -2
- package/dist/commands/sandbox/inspect.d.ts.map +0 -1
- package/dist/commands/sandbox/inspect.js +0 -18
- package/dist/commands/sandbox/lifecycle.d.ts +0 -7
- package/dist/commands/sandbox/lifecycle.d.ts.map +0 -1
- package/dist/commands/sandbox/lifecycle.js +0 -68
- package/dist/commands/sandbox/model.d.ts +0 -10
- package/dist/commands/sandbox/model.d.ts.map +0 -1
- package/dist/commands/sandbox/model.js +0 -37
- package/dist/commands/sandbox/picker.d.ts +0 -20
- package/dist/commands/sandbox/picker.d.ts.map +0 -1
- package/dist/commands/sandbox/picker.js +0 -23
- package/dist/lib/dockerSandbox.d.ts +0 -43
- package/dist/lib/dockerSandbox.d.ts.map +0 -1
- package/dist/lib/dockerSandbox.js +0 -69
- package/dist/lib/sandboxGitDefaults.d.ts +0 -10
- package/dist/lib/sandboxGitDefaults.d.ts.map +0 -1
- package/dist/lib/sandboxGitDefaults.js +0 -31
package/README.md
CHANGED
|
@@ -83,15 +83,17 @@ In Linear, assign tickets to yourself and add an `agent-*` label (`agent-claude`
|
|
|
83
83
|
crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
|
|
84
84
|
crew doctor # check setup
|
|
85
85
|
crew status [<TICKET>] # inspect current state or one ticket
|
|
86
|
-
crew run # one-shot
|
|
86
|
+
crew run # one-shot orchestration
|
|
87
87
|
crew run --watch # poll forever
|
|
88
|
-
crew
|
|
88
|
+
crew start <TICKET> # provision + launch one ticket now
|
|
89
89
|
crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
|
|
90
|
-
crew
|
|
90
|
+
crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
|
|
91
91
|
crew resume <TICKET> # reopen a paused ticket
|
|
92
92
|
crew cleanup <TICKET> # tear down every worktree for a ticket
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
Deprecated aliases still work but print a warning and will be removed in the next major version: `crew interrupt` → `crew stop`, `crew run --ticket <TICKET>` → `crew start <TICKET>`, `crew doctor --ticket <TICKET>` → `crew status <TICKET>`.
|
|
96
|
+
|
|
95
97
|
## Configuration
|
|
96
98
|
|
|
97
99
|
Two keys are required; everything else has a default.
|
|
@@ -109,7 +111,7 @@ The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username`
|
|
|
109
111
|
- `agent-claude`, `agent-codex`, `agent-<name>` → that model.
|
|
110
112
|
- `agent-any` → the model with the most available session capacity.
|
|
111
113
|
- Unknown `agent-<name>` → falls back to `models.default` with a warning.
|
|
112
|
-
- No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew
|
|
114
|
+
- No `agent-*` label → ignored by `crew run`. Dispatch on demand with `crew start <TICKET>` (also falls back to `models.default`).
|
|
113
115
|
- Todo tickets blocked by non-terminal blockers are skipped until their blockers reach a terminal status.
|
|
114
116
|
|
|
115
117
|
Status classification uses Linear's workflow `state.type` (`unstarted`, `started`, `completed`, `canceled`, `duplicate`), so renamed status columns work without configuration. Parent issues with children are ignored — sub-issues are the work items.
|
|
@@ -136,12 +138,12 @@ Resolution order: `GROUNDCREW_CONFIG` → cosmiconfig project-walk from cwd (any
|
|
|
136
138
|
| `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once for this `crew` instance. |
|
|
137
139
|
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
138
140
|
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
139
|
-
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew
|
|
141
|
+
| `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`. |
|
|
140
142
|
| `models.definitions` | `{ claude, codex }` | Agent definitions. Additive merge with shipped defaults. |
|
|
141
143
|
| `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. |
|
|
142
144
|
| `models.definitions.<name>.color` | — | Color for the workspace status pill (cmux only; tmux silently drops it). |
|
|
143
145
|
| `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. |
|
|
144
|
-
| `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)
|
|
146
|
+
| `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. |
|
|
145
147
|
| `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. |
|
|
146
148
|
| `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. |
|
|
147
149
|
| `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. |
|
|
@@ -186,9 +188,17 @@ Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines
|
|
|
186
188
|
<details>
|
|
187
189
|
<summary>Docker Sandboxes (sdx) setup</summary>
|
|
188
190
|
|
|
189
|
-
Each model that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew
|
|
191
|
+
Each model that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew addresses the sandbox as `groundcrew-<agent>` (e.g. `groundcrew-claude`) and reuses one existing sandbox per agent across repos and tickets.
|
|
192
|
+
|
|
193
|
+
First-time setup is manual:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
sbx create --name groundcrew-claude claude <projectDir>
|
|
197
|
+
sbx exec -it groundcrew-claude claude auth login
|
|
198
|
+
sbx exec -it groundcrew-claude gh auth login
|
|
199
|
+
```
|
|
190
200
|
|
|
191
|
-
|
|
201
|
+
Replace `claude` with the sbx agent for the model and `<projectDir>` with `workspace.projectDir` from `crew.config.ts`. Manage lifecycle and auth with `sbx` directly (`sbx ls`, `sbx exec`, `sbx rm`). Groundcrew does not create, authenticate, regenerate, list, or remove sandboxes.
|
|
192
202
|
|
|
193
203
|
</details>
|
|
194
204
|
|
|
@@ -234,21 +244,30 @@ In Progress (state.type=started) — Multi-event extractor: year inference can p
|
|
|
234
244
|
|
|
235
245
|
</details>
|
|
236
246
|
|
|
237
|
-
### `crew
|
|
247
|
+
### `crew start <TICKET>`
|
|
248
|
+
|
|
249
|
+
Launches one ticket immediately, bypassing orchestrator eligibility. Use it to dispatch a specific ticket on demand — including unlabeled tickets that `crew run` ignores. (Replaces the deprecated `crew run --ticket <TICKET>`.)
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
crew start HRD-442
|
|
253
|
+
crew start HRD-442 --dry-run
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `crew stop <TICKET>`
|
|
238
257
|
|
|
239
|
-
Stops a live workspace pane while preserving the ticket worktree and branch. The manual pause button for cases where you need terminal capacity back, want to stop an agent that's going in the wrong direction, or need to inspect the diff before letting another agent continue.
|
|
258
|
+
Stops a live workspace pane while preserving the ticket worktree and branch. The manual pause button for cases where you need terminal capacity back, want to stop an agent that's going in the wrong direction, or need to inspect the diff before letting another agent continue. (Replaces the deprecated `crew interrupt <TICKET>`.)
|
|
240
259
|
|
|
241
260
|
```bash
|
|
242
|
-
crew
|
|
261
|
+
crew stop HRD-442 --reason "wrong implementation direction"
|
|
243
262
|
crew status HRD-442
|
|
244
263
|
crew resume HRD-442
|
|
245
264
|
```
|
|
246
265
|
|
|
247
|
-
The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present,
|
|
266
|
+
The command closes the cmux/tmux workspace if present, records local run state, and never tears down the worktree. If the workspace was already gone but the worktree is still present, stop records that fact so status can show the preserved branch.
|
|
248
267
|
|
|
249
268
|
### `crew resume <TICKET>`
|
|
250
269
|
|
|
251
|
-
Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew
|
|
270
|
+
Reopens an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists it fails and leaves re-dispatch to `crew start <ticket>`.
|
|
252
271
|
|
|
253
272
|
The resume prompt tells the agent to inspect git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
|
|
254
273
|
|
package/crew.config.example.ts
CHANGED
|
@@ -73,24 +73,11 @@ export default {
|
|
|
73
73
|
// // macOS when you need an agent to use Docker safely.
|
|
74
74
|
// local: { runner: "auto" },
|
|
75
75
|
//
|
|
76
|
-
// //
|
|
77
|
-
// //
|
|
78
|
-
// //
|
|
79
|
-
// //
|
|
80
|
-
// //
|
|
81
|
-
// // tools that should appear in every sandbox's picker; set
|
|
82
|
-
// // `kind: "agent"` to scope a recipe to a single sbx agent.
|
|
83
|
-
// sandbox: {
|
|
84
|
-
// authRecipes: {
|
|
85
|
-
// gcloud: {
|
|
86
|
-
// displayName: "gcloud",
|
|
87
|
-
// binary: "gcloud",
|
|
88
|
-
// loginArgs: ["auth", "login", "--no-launch-browser"],
|
|
89
|
-
// statusArgs: ["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"],
|
|
90
|
-
// authenticatedPattern: /@/,
|
|
91
|
-
// },
|
|
92
|
-
// },
|
|
93
|
-
// },
|
|
76
|
+
// // Groundcrew does not create or authenticate sdx sandboxes. For an sdx
|
|
77
|
+
// // model, create the matching sandbox yourself before first launch:
|
|
78
|
+
// // sbx create --name groundcrew-claude claude ~/dev/groundcrew
|
|
79
|
+
// // sbx exec -it groundcrew-claude claude auth login
|
|
80
|
+
// // sbx exec -it groundcrew-claude gh auth login
|
|
94
81
|
//
|
|
95
82
|
// prompts: {
|
|
96
83
|
// // Keep personal workflow instructions next to this config, for example
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuQA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CvD"}
|
package/dist/cli.js
CHANGED
|
@@ -5,16 +5,27 @@ import { initConfigCli } from "./commands/init.js";
|
|
|
5
5
|
import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
|
|
6
6
|
import { orchestrate } from "./commands/orchestrator.js";
|
|
7
7
|
import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
|
|
8
|
-
import { sandboxCli } from "./commands/sandbox/index.js";
|
|
9
8
|
import { setupReposCli } from "./commands/setupRepos.js";
|
|
10
9
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
11
10
|
import { statusCli } from "./commands/status.js";
|
|
12
11
|
import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
|
|
13
12
|
import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
|
|
14
|
-
import { errorMessage, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
|
|
13
|
+
import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
|
|
15
14
|
const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
16
15
|
const NUDGE_FETCH_TIMEOUT_MS = 1000;
|
|
16
|
+
const REMOVED_SANDBOX_COMMAND_MESSAGE = [
|
|
17
|
+
"`crew sandbox` is no longer supported.",
|
|
18
|
+
"Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
|
|
19
|
+
"Use the manual `sbx` workflow in README.md#docker-sandboxes-sdx-setup, then keep `models.definitions.<model>.sandbox.agent` in crew.config.ts so launches can address the existing sandbox.",
|
|
20
|
+
].join("\n");
|
|
17
21
|
const requireFromCli = createRequire(import.meta.url);
|
|
22
|
+
/**
|
|
23
|
+
* Prints a deprecation warning to stderr naming the canonical command and that
|
|
24
|
+
* the old form is removed in the next major, then lets the caller proceed.
|
|
25
|
+
*/
|
|
26
|
+
function warnDeprecated(forms) {
|
|
27
|
+
writeError(`crew ${forms.oldForm} is deprecated and will be removed in the next major version; use crew ${forms.newForm} instead.`);
|
|
28
|
+
}
|
|
18
29
|
function setupUsage() {
|
|
19
30
|
return "Usage: crew setup repos [--dry-run] [<repo>...]";
|
|
20
31
|
}
|
|
@@ -54,6 +65,16 @@ async function runCli(argv) {
|
|
|
54
65
|
await orchestrate({ watch, dryRun });
|
|
55
66
|
return;
|
|
56
67
|
}
|
|
68
|
+
warnDeprecated({ oldForm: "run --ticket", newForm: "start" });
|
|
69
|
+
await setupWorkspaceCli(ticket, { dryRun });
|
|
70
|
+
}
|
|
71
|
+
const START_USAGE = "crew start <ticket> [--dry-run]";
|
|
72
|
+
async function startCli(argv) {
|
|
73
|
+
const { dryRun, positionals } = parseDryRunPositionals(argv, START_USAGE);
|
|
74
|
+
const [ticket, ...extras] = positionals;
|
|
75
|
+
if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
|
|
76
|
+
throw new Error(`Usage: ${START_USAGE}`);
|
|
77
|
+
}
|
|
57
78
|
await setupWorkspaceCli(ticket, { dryRun });
|
|
58
79
|
}
|
|
59
80
|
async function upgradeCliInvoke(argv) {
|
|
@@ -80,7 +101,23 @@ async function maybeRunUpgradeNudge(metadata) {
|
|
|
80
101
|
writeError(message);
|
|
81
102
|
}
|
|
82
103
|
}
|
|
104
|
+
function doctorTicketAlias(argv) {
|
|
105
|
+
if (argv[0] !== "--ticket") {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const ticket = readTicketArgument(argv, 0, "doctor");
|
|
109
|
+
if (argv.length > 2) {
|
|
110
|
+
throw new Error("Usage: crew status [<ticket>]");
|
|
111
|
+
}
|
|
112
|
+
return ticket;
|
|
113
|
+
}
|
|
83
114
|
async function doctorCli(argv) {
|
|
115
|
+
const aliasTicket = doctorTicketAlias(argv);
|
|
116
|
+
if (aliasTicket !== undefined) {
|
|
117
|
+
warnDeprecated({ oldForm: "doctor --ticket", newForm: "status" });
|
|
118
|
+
await statusCli([aliasTicket]);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
84
121
|
if (argv.length > 0) {
|
|
85
122
|
throw new Error("Usage: crew doctor");
|
|
86
123
|
}
|
|
@@ -94,10 +131,15 @@ const SUBCOMMANDS = {
|
|
|
94
131
|
invoke: initConfigCli,
|
|
95
132
|
},
|
|
96
133
|
run: {
|
|
97
|
-
summary: "Run the orchestrator (one-shot by default)
|
|
98
|
-
usage: "[--watch] [--dry-run]
|
|
134
|
+
summary: "Run the orchestrator: poll sources and start eligible tickets (one-shot by default)",
|
|
135
|
+
usage: "[--watch] [--dry-run]",
|
|
99
136
|
invoke: runCli,
|
|
100
137
|
},
|
|
138
|
+
start: {
|
|
139
|
+
summary: "Launch one ticket immediately, bypassing eligibility",
|
|
140
|
+
usage: "<ticket> [--dry-run]",
|
|
141
|
+
invoke: startCli,
|
|
142
|
+
},
|
|
101
143
|
doctor: {
|
|
102
144
|
summary: "Verify host prerequisites (PATH tools, config validity, Linear reachability)",
|
|
103
145
|
usage: "",
|
|
@@ -113,21 +155,25 @@ const SUBCOMMANDS = {
|
|
|
113
155
|
usage: "[--force] <ticket>",
|
|
114
156
|
invoke: cleanupWorkspaceCli,
|
|
115
157
|
},
|
|
116
|
-
|
|
158
|
+
stop: {
|
|
117
159
|
summary: "Stop a live ticket workspace while preserving its worktree",
|
|
118
160
|
usage: "<ticket> [--reason <text>]",
|
|
119
161
|
invoke: interruptWorkspaceCli,
|
|
120
162
|
},
|
|
163
|
+
interrupt: {
|
|
164
|
+
summary: "Deprecated alias for `crew stop`",
|
|
165
|
+
usage: "<ticket> [--reason <text>]",
|
|
166
|
+
deprecated: true,
|
|
167
|
+
invoke: async (argv) => {
|
|
168
|
+
warnDeprecated({ oldForm: "interrupt", newForm: "stop" });
|
|
169
|
+
await interruptWorkspaceCli(argv);
|
|
170
|
+
},
|
|
171
|
+
},
|
|
121
172
|
resume: {
|
|
122
173
|
summary: "Reopen an existing ticket worktree with a continuation prompt",
|
|
123
174
|
usage: "<ticket>",
|
|
124
175
|
invoke: resumeWorkspaceCli,
|
|
125
176
|
},
|
|
126
|
-
sandbox: {
|
|
127
|
-
summary: "Manage Docker Sandboxes (sbx) for configured models",
|
|
128
|
-
usage: "<list|ensure|regenerate|auth|rm> [...args]",
|
|
129
|
-
invoke: sandboxCli,
|
|
130
|
-
},
|
|
131
177
|
setup: {
|
|
132
178
|
summary: "Project-level setup commands (currently: repos)",
|
|
133
179
|
usage: "repos [--dry-run] [<repo>...]",
|
|
@@ -148,6 +194,9 @@ function printHelp() {
|
|
|
148
194
|
writeOutput("");
|
|
149
195
|
writeOutput("Commands:");
|
|
150
196
|
for (const [name, command] of Object.entries(SUBCOMMANDS)) {
|
|
197
|
+
if (command.deprecated === true) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
151
200
|
writeOutput(` ${name.padEnd(width)} ${command.summary}`);
|
|
152
201
|
writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
|
|
153
202
|
}
|
|
@@ -174,6 +223,11 @@ export async function run(argv) {
|
|
|
174
223
|
writeOutput(packageVersion());
|
|
175
224
|
return;
|
|
176
225
|
}
|
|
226
|
+
if (subcommand === "sandbox") {
|
|
227
|
+
writeError(REMOVED_SANDBOX_COMMAND_MESSAGE);
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
177
231
|
const command = SUBCOMMANDS[subcommand];
|
|
178
232
|
if (!command) {
|
|
179
233
|
writeError(`Unknown command: ${subcommand}\n`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAqGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
|
|
@@ -15,20 +15,20 @@ function parseArguments(argv) {
|
|
|
15
15
|
if (argument === "--reason") {
|
|
16
16
|
const value = argv[index + 1];
|
|
17
17
|
if (value === undefined || value.length === 0 || value.startsWith("-")) {
|
|
18
|
-
throw new Error("crew
|
|
18
|
+
throw new Error("crew stop --reason: reason text is required");
|
|
19
19
|
}
|
|
20
20
|
reason = value;
|
|
21
21
|
index += 1;
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
if (argument.startsWith("-")) {
|
|
25
|
-
throw new Error(`Unknown option: ${argument}\nUsage: crew
|
|
25
|
+
throw new Error(`Unknown option: ${argument}\nUsage: crew stop <ticket> [--reason <text>]`);
|
|
26
26
|
}
|
|
27
27
|
positionals.push(argument);
|
|
28
28
|
}
|
|
29
29
|
const [ticket, ...extras] = positionals;
|
|
30
30
|
if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
|
|
31
|
-
throw new Error("Usage: crew
|
|
31
|
+
throw new Error("Usage: crew stop <ticket> [--reason <text>]");
|
|
32
32
|
}
|
|
33
33
|
return { ticket: ticket.toLowerCase(), ...(reason === undefined ? {} : { reason }) };
|
|
34
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;CAChB;AA6HD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;CAChB;AA6HD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
2
2
|
import { loadConfig } from "../lib/config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
4
4
|
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
5
5
|
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
6
6
|
import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
@@ -117,7 +117,6 @@ export async function resumeWorkspace(config, options) {
|
|
|
117
117
|
text: renderResumePrompt(context),
|
|
118
118
|
});
|
|
119
119
|
const secretsFile = stageBuildSecrets(stagedPrompt.directory);
|
|
120
|
-
await ensureAgentSandbox({ config, definition, sandboxName });
|
|
121
120
|
const launchCommand = buildLaunchCommand({
|
|
122
121
|
definition,
|
|
123
122
|
promptFile: stagedPrompt.file,
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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"}
|
|
@@ -10,7 +10,7 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
10
10
|
import { runCommandAsync } from "../lib/commandRunner.js";
|
|
11
11
|
import { loadConfig } from "../lib/config.js";
|
|
12
12
|
import { which } from "../lib/host.js";
|
|
13
|
-
import { errorMessage, log, writeOutput } from "../lib/util.js";
|
|
13
|
+
import { errorMessage, log, parseDryRunPositionals, writeOutput } from "../lib/util.js";
|
|
14
14
|
function emptyResult() {
|
|
15
15
|
return {
|
|
16
16
|
existing: [],
|
|
@@ -190,18 +190,7 @@ export async function setupRepos(config, options) {
|
|
|
190
190
|
return result;
|
|
191
191
|
}
|
|
192
192
|
function parseArguments(argv) {
|
|
193
|
-
|
|
194
|
-
const positionals = [];
|
|
195
|
-
for (const argument of argv) {
|
|
196
|
-
if (argument === "--dry-run") {
|
|
197
|
-
dryRun = true;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (argument.startsWith("-")) {
|
|
201
|
-
throw new Error(`Unknown option: ${argument}\nUsage: crew setup repos [--dry-run] [<repo>...]`);
|
|
202
|
-
}
|
|
203
|
-
positionals.push(argument);
|
|
204
|
-
}
|
|
193
|
+
const { dryRun, positionals } = parseDryRunPositionals(argv, "crew setup repos [--dry-run] [<repo>...]");
|
|
205
194
|
const options = { dryRun };
|
|
206
195
|
if (positionals.length > 0) {
|
|
207
196
|
options.only = positionals;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAqBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAqBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAsGf;AAwHD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { fetchResolvedIssue } from "../lib/boardSource.js";
|
|
3
3
|
import { loadConfig } from "../lib/config.js";
|
|
4
|
-
import {
|
|
4
|
+
import { openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
5
5
|
import { buildLaunchCommand } from "../lib/launchCommand.js";
|
|
6
6
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
7
7
|
import { recordRunState } from "../lib/runState.js";
|
|
@@ -80,12 +80,6 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
80
80
|
const stagedPrompt = stagePrompt({ config, ticket, ticketDetails, worktreeName });
|
|
81
81
|
promptDir = stagedPrompt.directory;
|
|
82
82
|
const secretsFile = stageBuildSecrets(promptDir);
|
|
83
|
-
await ensureAgentSandbox({
|
|
84
|
-
config,
|
|
85
|
-
definition,
|
|
86
|
-
sandboxName,
|
|
87
|
-
...(signal === undefined ? {} : { signal }),
|
|
88
|
-
});
|
|
89
83
|
const launchCommand = buildLaunchCommand({
|
|
90
84
|
definition,
|
|
91
85
|
promptFile: stagedPrompt.file,
|
|
@@ -10,12 +10,6 @@ export declare function prepareAgentLaunch(input: {
|
|
|
10
10
|
purpose: "runs" | "resumes";
|
|
11
11
|
signal?: AbortSignal;
|
|
12
12
|
}): Promise<PreparedAgentLaunch>;
|
|
13
|
-
export declare function ensureAgentSandbox(input: {
|
|
14
|
-
config: ResolvedConfig;
|
|
15
|
-
definition: ModelDefinition;
|
|
16
|
-
sandboxName: string | undefined;
|
|
17
|
-
signal?: AbortSignal;
|
|
18
|
-
}): Promise<void>;
|
|
19
13
|
export declare function openAgentWorkspace(input: {
|
|
20
14
|
config: ResolvedConfig;
|
|
21
15
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhF,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,CA6B/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,8 +1,7 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import { ensureClearance } from "@clipboard-health/clearance";
|
|
3
|
-
import { ensureSandbox, sandboxNameFor } from "./dockerSandbox.js";
|
|
4
2
|
import { detectHostCapabilities } from "./host.js";
|
|
5
3
|
import { assertLocalRunnerRequirements, resolveLocalRunner } from "./localRunner.js";
|
|
4
|
+
import { sandboxNameFor } from "./sandboxName.js";
|
|
6
5
|
import { log, sleep } from "./util.js";
|
|
7
6
|
import { workspaces } from "./workspaces.js";
|
|
8
7
|
export async function prepareAgentLaunch(input) {
|
|
@@ -31,16 +30,6 @@ export async function prepareAgentLaunch(input) {
|
|
|
31
30
|
: undefined;
|
|
32
31
|
return { runner, sandboxName };
|
|
33
32
|
}
|
|
34
|
-
export async function ensureAgentSandbox(input) {
|
|
35
|
-
if (input.sandboxName !== undefined && input.definition.sandbox !== undefined) {
|
|
36
|
-
await ensureSandbox({
|
|
37
|
-
sandboxName: input.sandboxName,
|
|
38
|
-
sandbox: input.definition.sandbox,
|
|
39
|
-
mountPath: resolve(input.config.workspace.projectDir),
|
|
40
|
-
gitDefaults: input.config.sandbox.gitDefaults,
|
|
41
|
-
}, input.signal);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
33
|
export async function openAgentWorkspace(input) {
|
|
45
34
|
const spec = {
|
|
46
35
|
name: input.name,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmux Workspace backend. cmux is the macOS TUI; workspaces surface in its
|
|
3
|
+
* own app, so `accessHint` has nothing concise to emit. cmux can paint a
|
|
4
|
+
* per-workspace status pill, which `open` applies best-effort.
|
|
5
|
+
*/
|
|
6
|
+
import { type Adapter } from "./workspaceAdapter.ts";
|
|
7
|
+
export declare const cmuxAdapter: Adapter;
|
|
8
|
+
//# sourceMappingURL=cmuxAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmuxAdapter.d.ts","sourceRoot":"","sources":["../../src/lib/cmuxAdapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,WAAW,EAAE,OA6EzB,CAAC"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmux Workspace backend. cmux is the macOS TUI; workspaces surface in its
|
|
3
|
+
* own app, so `accessHint` has nothing concise to emit. cmux can paint a
|
|
4
|
+
* per-workspace status pill, which `open` applies best-effort.
|
|
5
|
+
*/
|
|
6
|
+
import { isSignalAborted, runWorkspaceCommand, } from "./workspaceAdapter.js";
|
|
7
|
+
import { errorMessage, log } from "./util.js";
|
|
8
|
+
export const cmuxAdapter = {
|
|
9
|
+
async open(spec, signal) {
|
|
10
|
+
const output = await runWorkspaceCommand("cmux", [
|
|
11
|
+
"--json",
|
|
12
|
+
"new-workspace",
|
|
13
|
+
"--name",
|
|
14
|
+
spec.name,
|
|
15
|
+
"--cwd",
|
|
16
|
+
spec.cwd,
|
|
17
|
+
"--command",
|
|
18
|
+
spec.command,
|
|
19
|
+
], signal);
|
|
20
|
+
const workspaceId = extractCmuxOpenId(output);
|
|
21
|
+
if (workspaceId === undefined) {
|
|
22
|
+
log(`cmux new-workspace returned unrecognized output for ${spec.name}; if a workspace was created, run \`cmux close-workspace\` manually.`);
|
|
23
|
+
throw new Error(`Unexpected cmux output: ${output}`);
|
|
24
|
+
}
|
|
25
|
+
if (spec.status !== undefined) {
|
|
26
|
+
try {
|
|
27
|
+
await applyCmuxStatus(workspaceId, spec.status, signal);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Status pills are best-effort. cmux v2+ dropped `set-status` entirely,
|
|
31
|
+
// so swallow that specific gap silently; surface anything else so a real
|
|
32
|
+
// regression doesn't hide behind the same swallow.
|
|
33
|
+
if (!isCmuxSetStatusUnsupported(error)) {
|
|
34
|
+
log(`cmux set-status failed for ${spec.name} (continuing): ${errorMessage(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async list(signal) {
|
|
40
|
+
const raw = await listCmuxRaw(signal);
|
|
41
|
+
return raw?.map((ws) => ({ name: ws.title }));
|
|
42
|
+
},
|
|
43
|
+
async close(name, signal) {
|
|
44
|
+
const raw = await listCmuxRaw(signal);
|
|
45
|
+
if (raw === undefined) {
|
|
46
|
+
// cmux v2 `workspace.close` rejects titles, so forwarding `name`
|
|
47
|
+
// would always fail. The list failure has already been logged by
|
|
48
|
+
// `listCmuxRaw`; bail rather than guarantee a downstream error.
|
|
49
|
+
log(`cmux close-workspace skipped for ${name}: list-workspaces failed, no usable id`);
|
|
50
|
+
return { kind: "unavailable" };
|
|
51
|
+
}
|
|
52
|
+
const match = raw.find((ws) => ws.title === name);
|
|
53
|
+
if (match === undefined) {
|
|
54
|
+
return { kind: "missing" };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await closeCmuxWorkspace(match.id, signal);
|
|
58
|
+
return { kind: "closed" };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (isSignalAborted(signal)) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const remaining = await listCmuxRaw(signal);
|
|
65
|
+
if (remaining === undefined) {
|
|
66
|
+
return { kind: "unavailable", error };
|
|
67
|
+
}
|
|
68
|
+
const isStillPresent = remaining.some((ws) => ws.title === name);
|
|
69
|
+
if (!isStillPresent) {
|
|
70
|
+
return { kind: "closed" };
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
accessHint(_name) {
|
|
76
|
+
// cmux is a TUI; users surface workspaces by launching the cmux app,
|
|
77
|
+
// not a shell command. No useful hint to emit.
|
|
78
|
+
// oxlint-disable-next-line unicorn/no-useless-undefined -- explicit signal that the backend has no hint
|
|
79
|
+
return undefined;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
function parseCmuxList(output) {
|
|
83
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json list-workspaces always emits this shape
|
|
84
|
+
const parsed = JSON.parse(output);
|
|
85
|
+
const items = [];
|
|
86
|
+
/* v8 ignore next @preserve -- cmux always emits a workspaces field; default keeps the loop safe */
|
|
87
|
+
for (const ws of parsed.workspaces ?? []) {
|
|
88
|
+
if (typeof ws.title !== "string" || ws.title.length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const id = pickCmuxId(ws);
|
|
92
|
+
if (id === undefined) {
|
|
93
|
+
log(`cmux list-workspaces returned workspace "${ws.title}" without a usable id or ref; skipping`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
items.push({ title: ws.title, id });
|
|
97
|
+
}
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The stable workspace handle cmux v2 expects in JSON-RPC params. Prefer
|
|
102
|
+
* the UUID; fall back to the legacy `workspace:N` short ref when older
|
|
103
|
+
* cmux builds don't surface it. Returns `undefined` when neither is
|
|
104
|
+
* available — cmux v2 `workspace.close` rejects titles, so we must never
|
|
105
|
+
* forward `title` as a workspace handle.
|
|
106
|
+
*/
|
|
107
|
+
function pickCmuxId(ws) {
|
|
108
|
+
if (typeof ws.id === "string" && ws.id.length > 0) {
|
|
109
|
+
return ws.id;
|
|
110
|
+
}
|
|
111
|
+
if (typeof ws.ref === "string" && ws.ref.length > 0) {
|
|
112
|
+
return ws.ref;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
async function listCmuxRaw(signal) {
|
|
117
|
+
try {
|
|
118
|
+
return parseCmuxList(await runWorkspaceCommand("cmux", ["--json", "list-workspaces"], signal));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (isSignalAborted(signal)) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
log(`cmux list-workspaces failed: ${errorMessage(error)}`);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function extractCmuxOpenId(output) {
|
|
129
|
+
try {
|
|
130
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cmux --json prints a workspace_id/ref object
|
|
131
|
+
const parsed = JSON.parse(output);
|
|
132
|
+
const uuid = parsed.workspace_id ?? parsed.id ?? "";
|
|
133
|
+
if (uuid.length > 0) {
|
|
134
|
+
return uuid;
|
|
135
|
+
}
|
|
136
|
+
const ref = parsed.workspace_ref ?? parsed.ref ?? "";
|
|
137
|
+
if (ref.length > 0) {
|
|
138
|
+
return ref;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* not JSON; fall through to regex */
|
|
143
|
+
}
|
|
144
|
+
const match = /workspace:\d+/.exec(output);
|
|
145
|
+
return match ? match[0] : undefined;
|
|
146
|
+
}
|
|
147
|
+
async function applyCmuxStatus(workspaceId, status, signal) {
|
|
148
|
+
const arguments_ = ["set-status", "model", status.text];
|
|
149
|
+
if (status.icon !== undefined) {
|
|
150
|
+
arguments_.push("--icon", status.icon);
|
|
151
|
+
}
|
|
152
|
+
if (status.color !== undefined) {
|
|
153
|
+
arguments_.push("--color", status.color);
|
|
154
|
+
}
|
|
155
|
+
arguments_.push("--workspace", workspaceId);
|
|
156
|
+
await runWorkspaceCommand("cmux", arguments_, signal);
|
|
157
|
+
}
|
|
158
|
+
async function closeCmuxWorkspace(workspaceId, signal) {
|
|
159
|
+
await runWorkspaceCommand("cmux", ["close-workspace", "--workspace", workspaceId], signal);
|
|
160
|
+
}
|
|
161
|
+
function isCmuxSetStatusUnsupported(error) {
|
|
162
|
+
return errorMessage(error).includes('unknown command "set-status"');
|
|
163
|
+
}
|