@clipboard-health/groundcrew 2.0.0 → 2.1.1
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 +6 -4
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +15 -8
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +24 -5
- package/dist/lib/util.d.ts +7 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +14 -4
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +61 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,18 +49,20 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
|
|
|
49
49
|
|
|
50
50
|
`crew` resolves the config path as: `GROUNDCREW_CONFIG` if set → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` if it exists → a `config.ts` sitting next to `crew`'s own source files (only useful from a local checkout; see [Hacking on groundcrew](#hacking-on-groundcrew)). Set `GROUNDCREW_CONFIG` only when you want to override the XDG location.
|
|
51
51
|
|
|
52
|
-
4. **Provide a Linear API key.** `crew`
|
|
52
|
+
4. **Provide a Linear API key.** `crew` reads the key from `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. Prefer `GROUNDCREW_LINEAR_API_KEY` so the value does not clash with other tools that consume `LINEAR_API_KEY`. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
# Direct
|
|
56
|
-
export
|
|
56
|
+
export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
|
|
57
57
|
crew doctor
|
|
58
58
|
|
|
59
59
|
# Via 1Password CLI (`op`), if you keep the key in a vault
|
|
60
|
-
echo "
|
|
60
|
+
echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
|
|
61
61
|
op run --env-file .env.1password -- crew doctor
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
`LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
|
|
65
|
+
|
|
64
66
|
5. **Prepare the runner and agent auth.** Groundcrew supports one runner: a `cmux` or `tmux` workspace on macOS, with Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
|
|
65
67
|
|
|
66
68
|
Setup fails before creating a worktree when the host is not macOS or `safehouse` is missing. `models.isolation`, per-model `isolation`, and per-model `sandbox` are legacy keys and now fail config validation.
|
|
@@ -185,7 +187,7 @@ For developers working on the package itself, clone this repo, run `npm install`
|
|
|
185
187
|
cd ~/dev/c/groundcrew
|
|
186
188
|
node --run crew -- doctor
|
|
187
189
|
|
|
188
|
-
# With 1Password for
|
|
190
|
+
# With 1Password for GROUNDCREW_LINEAR_API_KEY:
|
|
189
191
|
node --run crew:op -- run --watch
|
|
190
192
|
```
|
|
191
193
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync, statSync } from "node:fs";
|
|
6
6
|
import { loadConfig } from "../lib/config.js";
|
|
7
7
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
8
|
-
import { errorMessage,
|
|
8
|
+
import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
|
|
9
9
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
10
10
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
11
11
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
@@ -23,14 +23,21 @@ async function checkCmd(cmd, required, hint) {
|
|
|
23
23
|
}
|
|
24
24
|
return result;
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
|
|
26
|
+
function checkLinearApiKey() {
|
|
27
|
+
const resolved = resolveLinearApiKey();
|
|
28
|
+
if (resolved !== undefined) {
|
|
29
|
+
return {
|
|
30
|
+
name: "linear api key",
|
|
31
|
+
ok: true,
|
|
32
|
+
required: true,
|
|
33
|
+
hint: `set via $${resolved.source}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
29
36
|
return {
|
|
30
|
-
name:
|
|
31
|
-
ok:
|
|
37
|
+
name: "linear api key",
|
|
38
|
+
ok: false,
|
|
32
39
|
required: true,
|
|
33
|
-
hint:
|
|
40
|
+
hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
function checkDir(path, label) {
|
|
@@ -138,7 +145,7 @@ export async function doctor() {
|
|
|
138
145
|
const workspaceOutcome = resolveWorkspaceOutcome(config, host);
|
|
139
146
|
reportWorkspaceKind(config, workspaceOutcome);
|
|
140
147
|
const checks = [
|
|
141
|
-
|
|
148
|
+
checkLinearApiKey(),
|
|
142
149
|
await checkCmd("git", true, "https://git-scm.com/"),
|
|
143
150
|
...(await workspaceChecks(workspaceOutcome)),
|
|
144
151
|
checkDir(config.workspace.projectDir, "workspace.projectDir"),
|
|
@@ -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
|
@@ -41,7 +41,7 @@ async function verifyProject(client, config) {
|
|
|
41
41
|
const { projects } = response.data;
|
|
42
42
|
const [project] = projects.nodes;
|
|
43
43
|
if (!project) {
|
|
44
|
-
throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that
|
|
44
|
+
throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that your Linear API key can access this workspace.`);
|
|
45
45
|
}
|
|
46
46
|
log(`Resolved Linear project: ${project.name} (slugId ${project.slugId})`);
|
|
47
47
|
}
|
|
@@ -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/util.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export declare function log(message: string): void;
|
|
|
8
8
|
type LogEventFieldValue = boolean | number | string | readonly string[] | undefined;
|
|
9
9
|
export declare function logEvent(event: string, fields: Record<string, LogEventFieldValue>): void;
|
|
10
10
|
export declare function readEnvironmentVariable(name: string): string | undefined;
|
|
11
|
+
declare const LINEAR_API_KEY_SOURCES: readonly ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
|
|
12
|
+
export type LinearApiKeySource = (typeof LINEAR_API_KEY_SOURCES)[number];
|
|
13
|
+
export interface ResolvedLinearApiKey {
|
|
14
|
+
value: string;
|
|
15
|
+
source: LinearApiKeySource;
|
|
16
|
+
}
|
|
17
|
+
export declare function resolveLinearApiKey(): ResolvedLinearApiKey | undefined;
|
|
11
18
|
export declare function getLinearClient(): LinearClient;
|
|
12
19
|
export declare function errorMessage(error: unknown): string;
|
|
13
20
|
export {};
|
package/dist/lib/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,wBAAgB,eAAe,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,QAAA,MAAM,sBAAsB,YAAI,2BAA2B,EAAE,gBAAgB,CAAU,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,GAAG,SAAS,CAQtE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
|
package/dist/lib/util.js
CHANGED
|
@@ -87,12 +87,22 @@ export function readEnvironmentVariable(name) {
|
|
|
87
87
|
// oxlint-disable-next-line node/no-process-env -- Centralized environment accessor.
|
|
88
88
|
return process.env[name];
|
|
89
89
|
}
|
|
90
|
+
const LINEAR_API_KEY_SOURCES = ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
|
|
91
|
+
export function resolveLinearApiKey() {
|
|
92
|
+
for (const source of LINEAR_API_KEY_SOURCES) {
|
|
93
|
+
const value = readEnvironmentVariable(source);
|
|
94
|
+
if (value !== undefined && value.length > 0) {
|
|
95
|
+
return { value, source };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
90
100
|
export function getLinearClient() {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
throw new Error("
|
|
101
|
+
const resolved = resolveLinearApiKey();
|
|
102
|
+
if (resolved === undefined) {
|
|
103
|
+
throw new Error("Linear API key not set. Set GROUNDCREW_LINEAR_API_KEY or LINEAR_API_KEY in your environment.");
|
|
94
104
|
}
|
|
95
|
-
return new LinearClient({ apiKey });
|
|
105
|
+
return new LinearClient({ apiKey: resolved.value });
|
|
96
106
|
}
|
|
97
107
|
export function errorMessage(error) {
|
|
98
108
|
if (error instanceof Error) {
|
|
@@ -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,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,CAUxB;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
|
@@ -159,7 +159,28 @@ async function removeWorktree(config, entry, options) {
|
|
|
159
159
|
removeArguments.push("--force");
|
|
160
160
|
}
|
|
161
161
|
removeArguments.push(entry.dir);
|
|
162
|
-
|
|
162
|
+
try {
|
|
163
|
+
await runCommandAsync("git", removeArguments, longRunningCommandOptions(options.signal));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
// git's `fatal: ... use --force to delete it` line goes to inherited
|
|
167
|
+
// stderr, so the captured error is just "Exit status: 128". Probe the
|
|
168
|
+
// worktree ourselves so the failure message explains the condition
|
|
169
|
+
// (modified/untracked files) and points at `crew cleanup --force`.
|
|
170
|
+
if (options.force || options.signal?.aborted === true) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
|
|
174
|
+
if (dirtiness.kind !== "dirty") {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(describeDirtyWorktree({
|
|
178
|
+
ticket: entry.ticket,
|
|
179
|
+
dir: entry.dir,
|
|
180
|
+
modified: dirtiness.modified,
|
|
181
|
+
untracked: dirtiness.untracked,
|
|
182
|
+
}), { cause: error });
|
|
183
|
+
}
|
|
163
184
|
}
|
|
164
185
|
else {
|
|
165
186
|
log(`Worktree directory ${entry.dir} not found, pruning stale refs...`);
|
|
@@ -172,6 +193,45 @@ async function removeWorktree(config, entry, options) {
|
|
|
172
193
|
...signalProperty(options.signal),
|
|
173
194
|
});
|
|
174
195
|
}
|
|
196
|
+
async function probeWorktreeDirtiness(worktreeDir, signal) {
|
|
197
|
+
let output;
|
|
198
|
+
try {
|
|
199
|
+
output = await runCommandAsync("git", ["-C", worktreeDir, "status", "--porcelain"], signalProperty(signal));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { kind: "unknown" };
|
|
203
|
+
}
|
|
204
|
+
let modified = 0;
|
|
205
|
+
let untracked = 0;
|
|
206
|
+
for (const line of output.split("\n")) {
|
|
207
|
+
if (line.length === 0) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (line.startsWith("??")) {
|
|
211
|
+
untracked += 1;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
modified += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (modified === 0 && untracked === 0) {
|
|
218
|
+
return { kind: "clean" };
|
|
219
|
+
}
|
|
220
|
+
return { kind: "dirty", modified, untracked };
|
|
221
|
+
}
|
|
222
|
+
function describeDirtyWorktree(arguments_) {
|
|
223
|
+
const { ticket, dir, modified, untracked } = arguments_;
|
|
224
|
+
const parts = [];
|
|
225
|
+
if (modified > 0) {
|
|
226
|
+
parts.push(`${modified} modified file${modified === 1 ? "" : "s"}`);
|
|
227
|
+
}
|
|
228
|
+
if (untracked > 0) {
|
|
229
|
+
parts.push(`${untracked} untracked file${untracked === 1 ? "" : "s"}`);
|
|
230
|
+
}
|
|
231
|
+
const summary = parts.join(" and ");
|
|
232
|
+
const pronoun = modified + untracked === 1 ? "it" : "them";
|
|
233
|
+
return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
|
|
234
|
+
}
|
|
175
235
|
function list(config) {
|
|
176
236
|
return listWorktrees(config);
|
|
177
237
|
}
|
package/package.json
CHANGED