@clipboard-health/groundcrew 4.3.3 → 4.3.5
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.
|
@@ -188,7 +188,7 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
|
|
|
188
188
|
* strips them); they're `unset` on the host after setup and not passed to the
|
|
189
189
|
* agent wrap. The host keeps `cd`, the prompt read, and a temporary
|
|
190
190
|
* command-named shim so Safehouse can select the intended agent profile while
|
|
191
|
-
* the actual agent command remains `sh -
|
|
191
|
+
* the actual agent command remains `sh -c`.
|
|
192
192
|
*/
|
|
193
193
|
function buildSafehouseLaunchCommand(arguments_) {
|
|
194
194
|
const promptDir = dirname(arguments_.promptFile);
|
|
@@ -207,16 +207,16 @@ function buildSafehouseLaunchCommand(arguments_) {
|
|
|
207
207
|
if (arguments_.secretsFile !== undefined) {
|
|
208
208
|
lines.push(sourceSecretsLine(arguments_.secretsFile));
|
|
209
209
|
}
|
|
210
|
-
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${envPassFlag}sh -
|
|
210
|
+
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `${safehouseWrapper} ${envPassFlag}sh -c ${shellSingleQuote(setupCommand)}`);
|
|
211
211
|
if (arguments_.secretsFile !== undefined) {
|
|
212
212
|
lines.push(unsetSecretsLine());
|
|
213
213
|
}
|
|
214
214
|
lines.push(`_safehouse_shim_dir=$(mktemp -d "\${TMPDIR:-/tmp}/groundcrew-safehouse-XXXXXX")`, `trap 'rm -rf "$_safehouse_shim_dir"' EXIT`, `_safehouse_shim="$_safehouse_shim_dir/${safehouseCommandName}"`, `ln -s /bin/sh "$_safehouse_shim"`,
|
|
215
215
|
// Safehouse selects an agent profile from the wrapped command's basename.
|
|
216
|
-
// Running the real launch chain as `sh -
|
|
216
|
+
// Running the real launch chain as `sh -c` would make it see `sh`, so use
|
|
217
217
|
// an agent-named symlink to /bin/sh. This preserves per-agent profile
|
|
218
218
|
// selection without enabling every agent profile.
|
|
219
|
-
`${safehouseWrapper} "$_safehouse_shim" -
|
|
219
|
+
`${safehouseWrapper} "$_safehouse_shim" -c ${shellSingleQuote(agentCommand)} sh "$_p"; _safehouse_status=$?; rm -rf "$_safehouse_shim_dir"; trap - EXIT; exit "$_safehouse_status"`);
|
|
220
220
|
return lines.join(" && ");
|
|
221
221
|
}
|
|
222
222
|
function buildSdxLaunchCommand(arguments_) {
|
|
@@ -248,6 +248,6 @@ function buildSdxLaunchCommand(arguments_) {
|
|
|
248
248
|
if (arguments_.secretsFile !== undefined) {
|
|
249
249
|
lines.push(sourceSecretsLine(arguments_.secretsFile));
|
|
250
250
|
}
|
|
251
|
-
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.worktreeDir)} ${shellSingleQuote(arguments_.sandboxName)} sh -
|
|
251
|
+
lines.push(`_p=$(cat ${shellSingleQuote(arguments_.promptFile)})`, `rm -rf ${shellSingleQuote(promptDir)}`, `exec sbx exec -it ${sbxEnvironmentFlags}-w ${shellSingleQuote(arguments_.worktreeDir)} ${shellSingleQuote(arguments_.sandboxName)} sh -c ${shellSingleQuote(innerCommand)} sh "$_p"`);
|
|
252
252
|
return lines.join(" && ");
|
|
253
253
|
}
|
|
@@ -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;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;
|
|
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;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAKlE,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;AAaD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AA8PD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,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;AAyBD,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,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* (workspace-close + worktree-remove paired) so callers don't reach into
|
|
8
8
|
* git directly.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
11
11
|
import { userInfo } from "node:os";
|
|
12
|
-
import { resolve } from "node:path";
|
|
12
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
13
13
|
import { runCommandAsync } from "./commandRunner.js";
|
|
14
14
|
import { resolveDefaultBranch } from "./defaultBranch.js";
|
|
15
15
|
import { errorMessage, log } from "./util.js";
|
|
16
16
|
import { workspaces } from "./workspaces.js";
|
|
17
17
|
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
18
|
+
const WORKTREE_LIST_PREFIX = "worktree ";
|
|
18
19
|
export class WorktreeAlreadyExistsError extends Error {
|
|
19
20
|
dir;
|
|
20
21
|
constructor(dir) {
|
|
@@ -186,26 +187,46 @@ async function removeWorktree(config, entry, options) {
|
|
|
186
187
|
// ourselves so the failure message names the condition — dirty
|
|
187
188
|
// (modified/untracked files, fixable with `crew cleanup --force`) or
|
|
188
189
|
// orphan (directory exists on disk but is not registered with the
|
|
189
|
-
// parent repo,
|
|
190
|
-
|
|
190
|
+
// parent repo, fixable with `crew cleanup --force` when the path still
|
|
191
|
+
// matches groundcrew's expected worktree location).
|
|
192
|
+
if (options.signal?.aborted === true) {
|
|
191
193
|
throw error;
|
|
192
194
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
if (options.force) {
|
|
196
|
+
const registration = await probeWorktreeRegistration({
|
|
197
|
+
repoDir,
|
|
198
|
+
worktreeDir: entry.dir,
|
|
199
|
+
...signalProperty(options.signal),
|
|
200
|
+
});
|
|
201
|
+
if (registration !== "orphan") {
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
removeOrphanWorktreeDirectory(config, entry);
|
|
201
205
|
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
throw new Error(
|
|
206
|
+
else {
|
|
207
|
+
const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
|
|
208
|
+
if (dirtiness.kind === "dirty") {
|
|
209
|
+
throw new Error(describeDirtyWorktree({
|
|
210
|
+
ticket: entry.ticket,
|
|
211
|
+
dir: entry.dir,
|
|
212
|
+
modified: dirtiness.modified,
|
|
213
|
+
untracked: dirtiness.untracked,
|
|
214
|
+
}), { cause: error });
|
|
206
215
|
}
|
|
216
|
+
if (dirtiness.kind === "unknown") {
|
|
217
|
+
const registration = await probeWorktreeRegistration({
|
|
218
|
+
repoDir,
|
|
219
|
+
worktreeDir: entry.dir,
|
|
220
|
+
...signalProperty(options.signal),
|
|
221
|
+
});
|
|
222
|
+
if (registration === "orphan") {
|
|
223
|
+
throw new Error(describeOrphanWorktree({ ticket: entry.ticket, dir: entry.dir }), {
|
|
224
|
+
cause: error,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
207
229
|
}
|
|
208
|
-
throw error;
|
|
209
230
|
}
|
|
210
231
|
}
|
|
211
232
|
else {
|
|
@@ -258,19 +279,47 @@ function describeDirtyWorktree(arguments_) {
|
|
|
258
279
|
const pronoun = modified + untracked === 1 ? "it" : "them";
|
|
259
280
|
return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
|
|
260
281
|
}
|
|
261
|
-
async function probeWorktreeRegistration(
|
|
282
|
+
async function probeWorktreeRegistration(arguments_) {
|
|
262
283
|
let output;
|
|
263
284
|
try {
|
|
264
|
-
output = await runCommandAsync("git", ["-C",
|
|
285
|
+
output = await runCommandAsync("git", ["-C", arguments_.repoDir, "worktree", "list", "--porcelain"], signalProperty(arguments_.signal));
|
|
265
286
|
}
|
|
266
287
|
catch {
|
|
267
288
|
return "unknown";
|
|
268
289
|
}
|
|
269
|
-
|
|
290
|
+
const resolvedWorktreeDir = resolve(arguments_.worktreeDir);
|
|
291
|
+
for (const line of output.split("\n")) {
|
|
292
|
+
if (!line.startsWith(WORKTREE_LIST_PREFIX)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (resolve(line.slice(WORKTREE_LIST_PREFIX.length)) === resolvedWorktreeDir) {
|
|
296
|
+
return "registered";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return "orphan";
|
|
270
300
|
}
|
|
271
301
|
function describeOrphanWorktree(arguments_) {
|
|
272
|
-
const { dir } = arguments_;
|
|
273
|
-
return `directory exists but is not a registered git worktree.
|
|
302
|
+
const { ticket, dir } = arguments_;
|
|
303
|
+
return `directory exists but is not a registered git worktree. Run \`crew cleanup --force ${ticket}\` to remove ${dir}, or inspect it first if it may contain valuable files.`;
|
|
304
|
+
}
|
|
305
|
+
function expectedHostWorktreeDir(config, entry) {
|
|
306
|
+
return resolve(config.workspace.projectDir, `${entry.repository}-${entry.ticket}`);
|
|
307
|
+
}
|
|
308
|
+
function isInsideDirectory(parentDir, childDir) {
|
|
309
|
+
const childRelativePath = relative(parentDir, childDir);
|
|
310
|
+
return (childRelativePath.length > 0 &&
|
|
311
|
+
!childRelativePath.startsWith("..") &&
|
|
312
|
+
!isAbsolute(childRelativePath));
|
|
313
|
+
}
|
|
314
|
+
function removeOrphanWorktreeDirectory(config, entry) {
|
|
315
|
+
const projectDir = resolve(config.workspace.projectDir);
|
|
316
|
+
const expectedDir = expectedHostWorktreeDir(config, entry);
|
|
317
|
+
const targetDir = resolve(entry.dir);
|
|
318
|
+
if (targetDir !== expectedDir || !isInsideDirectory(projectDir, targetDir)) {
|
|
319
|
+
throw new Error(`Refusing to force-delete ${entry.dir}: expected groundcrew worktree path ${expectedDir}.`);
|
|
320
|
+
}
|
|
321
|
+
log(`Removing orphaned worktree directory ${entry.dir} (--force)...`);
|
|
322
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
274
323
|
}
|
|
275
324
|
function list(config) {
|
|
276
325
|
return listWorktrees(config);
|
package/package.json
CHANGED