@clipboard-health/groundcrew 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +45 -4
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +23 -4
- package/dist/lib/workspaces.d.ts +5 -0
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +18 -2
- package/dist/lib/worktrees.d.ts +5 -0
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +75 -6
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,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;AAmED,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":"AAOA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvF,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,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;AAmED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA6Ef;AA0FD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -10,7 +10,7 @@ import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
|
10
10
|
import { assertLocalRunnerRequirements } from "../lib/localRunner.js";
|
|
11
11
|
import { errorMessage, getLinearClient, log, readEnvironmentVariable } from "../lib/util.js";
|
|
12
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
13
|
-
import { worktrees } from "../lib/worktrees.js";
|
|
13
|
+
import { isWorktreeAlreadyExistsError, worktrees } from "../lib/worktrees.js";
|
|
14
14
|
async function fetchTicket(ticket) {
|
|
15
15
|
const client = getLinearClient();
|
|
16
16
|
const issue = await client.issue(ticket.toUpperCase());
|
|
@@ -78,9 +78,19 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
78
78
|
assertLocalRunnerRequirements(await detectHostCapabilities(signal));
|
|
79
79
|
await ensureClearance({ logger: log });
|
|
80
80
|
const spec = { repository, ticket };
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
let created;
|
|
82
|
+
try {
|
|
83
|
+
created =
|
|
84
|
+
signal === undefined
|
|
85
|
+
? await worktrees.create(config, spec)
|
|
86
|
+
: await worktrees.create(config, spec, signal);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isWorktreeAlreadyExistsError(error)) {
|
|
90
|
+
await logAccessHintForExistingWorkspace({ config, ticket, signal });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
84
94
|
const { branchName, dir: launchDir } = created;
|
|
85
95
|
const worktreeName = `${repository}-${ticket}`;
|
|
86
96
|
// Anything that fails after the worktree is on disk must roll it back
|
|
@@ -120,12 +130,43 @@ export async function setupWorkspace(config, options, runOptions = {}) {
|
|
|
120
130
|
log(`Workspace "${ticket}" launched (${model})`);
|
|
121
131
|
log(` Worktree: ${launchDir}`);
|
|
122
132
|
log(` Branch: ${branchName}`);
|
|
133
|
+
await logWorkspaceAccessHint({ config, ticket, signal });
|
|
123
134
|
}
|
|
124
135
|
catch (error) {
|
|
125
136
|
await rollbackWorktree({ config, entry: created, promptDir });
|
|
126
137
|
throw error;
|
|
127
138
|
}
|
|
128
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Probe the workspace backend and, if a workspace for `ticket` is still
|
|
142
|
+
* live, log the access hint. Used on the pre-launch error path (e.g. the
|
|
143
|
+
* worktree already exists from a prior run) so the user can find the
|
|
144
|
+
* still-running session instead of being told only that the worktree is
|
|
145
|
+
* in the way. Silent when the probe is unavailable or the workspace is
|
|
146
|
+
* gone — we don't want to point at a window that doesn't exist.
|
|
147
|
+
*/
|
|
148
|
+
async function logAccessHintForExistingWorkspace(arguments_) {
|
|
149
|
+
const { config, ticket, signal } = arguments_;
|
|
150
|
+
const accessHint = await workspaces.accessHint(config, ticket, signal);
|
|
151
|
+
if (accessHint === undefined) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const probe = await workspaces.probe(config, signal);
|
|
155
|
+
if (probe.kind !== "ok" || !probe.names.has(ticket)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
logAccessHint(accessHint);
|
|
159
|
+
}
|
|
160
|
+
async function logWorkspaceAccessHint(arguments_) {
|
|
161
|
+
const accessHint = await workspaces.accessHint(arguments_.config, arguments_.ticket, arguments_.signal);
|
|
162
|
+
if (accessHint === undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
logAccessHint(accessHint);
|
|
166
|
+
}
|
|
167
|
+
function logAccessHint(accessHint) {
|
|
168
|
+
log(` Attach: ${accessHint.command}`);
|
|
169
|
+
}
|
|
129
170
|
async function rollbackWorktree(arguments_) {
|
|
130
171
|
log(`Setup failed; rolling back worktree ${arguments_.entry.repository}-${arguments_.entry.ticket}...`);
|
|
131
172
|
let result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;
|
|
1
|
+
{"version":3,"file":"boardSource.d.ts","sourceRoot":"","sources":["../../src/lib/boardSource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhF;AAmMD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmDzB"}
|
package/dist/lib/boardSource.js
CHANGED
|
@@ -163,7 +163,11 @@ function escapeRegex(value) {
|
|
|
163
163
|
// must beat `api` when both are configured. `\b` treats `-` as a word
|
|
164
164
|
// boundary, so without this ordering `api` would win on `api-admin`.
|
|
165
165
|
function buildRepositoryRegex(config) {
|
|
166
|
-
const
|
|
166
|
+
const candidates = config.workspace.knownRepositories.flatMap((repo) => {
|
|
167
|
+
const slashIndex = repo.indexOf("/");
|
|
168
|
+
return slashIndex === -1 ? [repo] : [repo, repo.slice(slashIndex + 1)];
|
|
169
|
+
});
|
|
170
|
+
const alternation = candidates
|
|
167
171
|
.toSorted((a, b) => b.length - a.length)
|
|
168
172
|
.map(escapeRegex)
|
|
169
173
|
.join("|");
|
|
@@ -223,14 +227,29 @@ function parseRepository(arguments_) {
|
|
|
223
227
|
repositories: config.workspace.knownRepositories,
|
|
224
228
|
});
|
|
225
229
|
}
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
230
|
+
const matched = repositoryRegex.exec(description)?.[1];
|
|
231
|
+
if (matched === undefined) {
|
|
228
232
|
throw new RepositoryResolutionError({
|
|
229
233
|
ticket,
|
|
230
234
|
repositories: config.workspace.knownRepositories,
|
|
231
235
|
});
|
|
232
236
|
}
|
|
233
|
-
|
|
237
|
+
// Resolve the match to a known repo. The regex may capture a bare repo name
|
|
238
|
+
// (no org prefix) when only that appears in the description; the filter
|
|
239
|
+
// handles both full "owner/repo" and bare "repo" matches. Reject if
|
|
240
|
+
// ambiguous (same bare name under multiple orgs).
|
|
241
|
+
const candidates = config.workspace.knownRepositories.filter((r) => r === matched || r.endsWith(`/${matched}`));
|
|
242
|
+
if (candidates.length !== 1) {
|
|
243
|
+
throw new RepositoryResolutionError({
|
|
244
|
+
ticket,
|
|
245
|
+
repositories: config.workspace.knownRepositories,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/* v8 ignore next 3 @preserve -- candidates.length===1 guarantees [0] is defined */
|
|
249
|
+
if (candidates[0] === undefined) {
|
|
250
|
+
throw new Error("unreachable");
|
|
251
|
+
}
|
|
252
|
+
return candidates[0];
|
|
234
253
|
}
|
|
235
254
|
function parseAgentLabels(labels, config) {
|
|
236
255
|
const agentLabels = labels.filter((label) => label.name.startsWith(AGENT_LABEL_PREFIX));
|
package/dist/lib/workspaces.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface WorkspaceStatus {
|
|
|
16
16
|
color?: string;
|
|
17
17
|
icon?: string;
|
|
18
18
|
}
|
|
19
|
+
export interface WorkspaceAccessHint {
|
|
20
|
+
kind: "attachCommand";
|
|
21
|
+
command: string;
|
|
22
|
+
}
|
|
19
23
|
export interface OpenSpec {
|
|
20
24
|
/** Ticket id; becomes the workspace's name. */
|
|
21
25
|
name: string;
|
|
@@ -53,6 +57,7 @@ export declare const workspaces: {
|
|
|
53
57
|
open(config: ResolvedConfig, spec: OpenSpec, signal?: AbortSignal): Promise<void>;
|
|
54
58
|
probe: typeof probeWorkspaces;
|
|
55
59
|
close(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<void>;
|
|
60
|
+
accessHint(config: ResolvedConfig, name: string, signal?: AbortSignal): Promise<WorkspaceAccessHint | undefined>;
|
|
56
61
|
};
|
|
57
62
|
export {};
|
|
58
63
|
//# sourceMappingURL=workspaces.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;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,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,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG1E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;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,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA+L7C,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;AA+ND,iBAAe,eAAe,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,eAAO,MAAM,UAAU;IACf,IAAI,SAAS,cAAc,QAAQ,QAAQ,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvF,KAAK;IACC,KAAK,SAAS,cAAc,QAAQ,MAAM,WAAW,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhF,UAAU,SACN,cAAc,QAChB,MAAM,WACH,WAAW,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAI5C,CAAC"}
|
package/dist/lib/workspaces.js
CHANGED
|
@@ -146,6 +146,12 @@ const cmuxAdapter = {
|
|
|
146
146
|
throw error;
|
|
147
147
|
}
|
|
148
148
|
},
|
|
149
|
+
accessHint(_name) {
|
|
150
|
+
// cmux is a TUI; users surface workspaces by launching the cmux app,
|
|
151
|
+
// not a shell command. No useful hint to emit.
|
|
152
|
+
// oxlint-disable-next-line unicorn/no-useless-undefined -- explicit signal that the backend has no hint
|
|
153
|
+
return undefined;
|
|
154
|
+
},
|
|
149
155
|
};
|
|
150
156
|
export function resolveWorkspaceKind(arguments_) {
|
|
151
157
|
const { config, host } = arguments_;
|
|
@@ -186,6 +192,9 @@ const TMUX_SESSION = "groundcrew";
|
|
|
186
192
|
// sentinel and filter it out — it stays around as a placeholder so the
|
|
187
193
|
// session doesn't collapse when the last ticket window closes.
|
|
188
194
|
const TMUX_IDLE_WINDOW = "_groundcrew_idle";
|
|
195
|
+
function tmuxTarget(name) {
|
|
196
|
+
return `${TMUX_SESSION}:${name}`;
|
|
197
|
+
}
|
|
189
198
|
function isTmuxNotFoundError(error) {
|
|
190
199
|
// runCommand surfaces the child's stderr in error.message, so the "no
|
|
191
200
|
// server" / "missing session" / "can't find window" signatures are visible
|
|
@@ -265,7 +274,7 @@ function parseTmuxWindows(output) {
|
|
|
265
274
|
const tmuxAdapter = {
|
|
266
275
|
async open(spec, signal) {
|
|
267
276
|
await ensureTmuxSession(signal);
|
|
268
|
-
const target =
|
|
277
|
+
const target = tmuxTarget(spec.name);
|
|
269
278
|
const keepDeadWindowsEnv = readEnvironmentVariable("GROUNDCREW_KEEP_DEAD_WINDOWS");
|
|
270
279
|
const keepDeadWindows = keepDeadWindowsEnv !== undefined && keepDeadWindowsEnv.length > 0;
|
|
271
280
|
await runWorkspaceCommand("tmux", [
|
|
@@ -307,7 +316,7 @@ const tmuxAdapter = {
|
|
|
307
316
|
},
|
|
308
317
|
async close(name, signal) {
|
|
309
318
|
try {
|
|
310
|
-
await runWorkspaceCommand("tmux", ["kill-window", "-t",
|
|
319
|
+
await runWorkspaceCommand("tmux", ["kill-window", "-t", tmuxTarget(name)], signal);
|
|
311
320
|
}
|
|
312
321
|
catch (error) {
|
|
313
322
|
if (isSignalAborted(signal)) {
|
|
@@ -319,6 +328,9 @@ const tmuxAdapter = {
|
|
|
319
328
|
throw error;
|
|
320
329
|
}
|
|
321
330
|
},
|
|
331
|
+
accessHint(name) {
|
|
332
|
+
return { kind: "attachCommand", command: `tmux attach -t ${tmuxTarget(name)}` };
|
|
333
|
+
},
|
|
322
334
|
};
|
|
323
335
|
// Per-config cache: production resolves the adapter once at first use
|
|
324
336
|
// (loadConfig returns a frozen, cached instance); each test uses a fresh
|
|
@@ -364,4 +376,8 @@ export const workspaces = {
|
|
|
364
376
|
const adapter = await adapterFor(config, signal);
|
|
365
377
|
await adapter.close(name, signal);
|
|
366
378
|
},
|
|
379
|
+
async accessHint(config, name, signal) {
|
|
380
|
+
const adapter = await adapterFor(config, signal);
|
|
381
|
+
return adapter.accessHint(name);
|
|
382
|
+
},
|
|
367
383
|
};
|
package/dist/lib/worktrees.d.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import type { ResolvedConfig } from "./config.ts";
|
|
11
11
|
import { type WorkspaceProbe } from "./workspaces.ts";
|
|
12
12
|
export type WorktreeKind = "host";
|
|
13
|
+
export declare class WorktreeAlreadyExistsError extends Error {
|
|
14
|
+
readonly dir: string;
|
|
15
|
+
constructor(dir: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function isWorktreeAlreadyExistsError(error: unknown): error is WorktreeAlreadyExistsError;
|
|
13
18
|
export interface WorktreeEntry {
|
|
14
19
|
repository: string;
|
|
15
20
|
/** Linear ticket id, lowercased — e.g. "team-220". */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoSD,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAKD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAkDzB;AAED,eAAO,MAAM,SAAS;;;;;;CAMrB,CAAC"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -14,6 +14,17 @@ import { runCommandAsync } from "./commandRunner.js";
|
|
|
14
14
|
import { errorMessage, log } from "./util.js";
|
|
15
15
|
import { workspaces } from "./workspaces.js";
|
|
16
16
|
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
17
|
+
export class WorktreeAlreadyExistsError extends Error {
|
|
18
|
+
dir;
|
|
19
|
+
constructor(dir) {
|
|
20
|
+
super(`Worktree already exists: ${dir}`);
|
|
21
|
+
this.dir = dir;
|
|
22
|
+
this.name = "WorktreeAlreadyExistsError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function isWorktreeAlreadyExistsError(error) {
|
|
26
|
+
return error instanceof WorktreeAlreadyExistsError;
|
|
27
|
+
}
|
|
17
28
|
const TICKET_RE = /^[a-z][\da-z]*-\d+$/;
|
|
18
29
|
const TICKET_DIR_RE = /^(.+)-([a-z][\da-z]*-\d+)$/;
|
|
19
30
|
function branchPrefix() {
|
|
@@ -159,7 +170,28 @@ async function removeWorktree(config, entry, options) {
|
|
|
159
170
|
removeArguments.push("--force");
|
|
160
171
|
}
|
|
161
172
|
removeArguments.push(entry.dir);
|
|
162
|
-
|
|
173
|
+
try {
|
|
174
|
+
await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// git's `fatal: ... use --force to delete it` line goes to inherited
|
|
178
|
+
// stderr, so the captured error is just "Exit status: 128". Probe the
|
|
179
|
+
// worktree ourselves so the failure message explains the condition
|
|
180
|
+
// (modified/untracked files) and points at `crew cleanup --force`.
|
|
181
|
+
if (options.force || options.signal?.aborted === true) {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
|
|
185
|
+
if (dirtiness.kind !== "dirty") {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(describeDirtyWorktree({
|
|
189
|
+
ticket: entry.ticket,
|
|
190
|
+
dir: entry.dir,
|
|
191
|
+
modified: dirtiness.modified,
|
|
192
|
+
untracked: dirtiness.untracked,
|
|
193
|
+
}), { cause: error });
|
|
194
|
+
}
|
|
163
195
|
}
|
|
164
196
|
else {
|
|
165
197
|
log(`Worktree directory ${entry.dir} not found, pruning stale refs...`);
|
|
@@ -172,6 +204,45 @@ async function removeWorktree(config, entry, options) {
|
|
|
172
204
|
...signalProperty(options.signal),
|
|
173
205
|
});
|
|
174
206
|
}
|
|
207
|
+
async function probeWorktreeDirtiness(worktreeDir, signal) {
|
|
208
|
+
let output;
|
|
209
|
+
try {
|
|
210
|
+
output = await runCommandAsync("git", ["-C", worktreeDir, "status", "--porcelain"], signalProperty(signal));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return { kind: "unknown" };
|
|
214
|
+
}
|
|
215
|
+
let modified = 0;
|
|
216
|
+
let untracked = 0;
|
|
217
|
+
for (const line of output.split("\n")) {
|
|
218
|
+
if (line.length === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (line.startsWith("??")) {
|
|
222
|
+
untracked += 1;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
modified += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (modified === 0 && untracked === 0) {
|
|
229
|
+
return { kind: "clean" };
|
|
230
|
+
}
|
|
231
|
+
return { kind: "dirty", modified, untracked };
|
|
232
|
+
}
|
|
233
|
+
function describeDirtyWorktree(arguments_) {
|
|
234
|
+
const { ticket, dir, modified, untracked } = arguments_;
|
|
235
|
+
const parts = [];
|
|
236
|
+
if (modified > 0) {
|
|
237
|
+
parts.push(`${modified} modified file${modified === 1 ? "" : "s"}`);
|
|
238
|
+
}
|
|
239
|
+
if (untracked > 0) {
|
|
240
|
+
parts.push(`${untracked} untracked file${untracked === 1 ? "" : "s"}`);
|
|
241
|
+
}
|
|
242
|
+
const summary = parts.join(" and ");
|
|
243
|
+
const pronoun = modified + untracked === 1 ? "it" : "them";
|
|
244
|
+
return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
|
|
245
|
+
}
|
|
175
246
|
function list(config) {
|
|
176
247
|
return listWorktrees(config);
|
|
177
248
|
}
|
|
@@ -179,11 +250,9 @@ function findByTicket(config, ticket) {
|
|
|
179
250
|
return list(config).filter((entry) => entry.ticket === ticket);
|
|
180
251
|
}
|
|
181
252
|
async function create(config, spec, signal) {
|
|
182
|
-
const existing = findByTicket(config, spec.ticket).
|
|
183
|
-
if (existing
|
|
184
|
-
|
|
185
|
-
/* v8 ignore next @preserve -- length>0 guarantees [0] is defined */
|
|
186
|
-
throw new Error(`Worktree already exists: ${first?.dir}`);
|
|
253
|
+
const existing = findByTicket(config, spec.ticket).find((entry) => entry.repository === spec.repository);
|
|
254
|
+
if (existing !== undefined) {
|
|
255
|
+
throw new WorktreeAlreadyExistsError(existing.dir);
|
|
187
256
|
}
|
|
188
257
|
return await createWorktree(config, spec, signal);
|
|
189
258
|
}
|
package/package.json
CHANGED