feat: add abortSignal to all commands

- Extend `listArchiveTags`, `getAvailableRemotes`, `deleteLocalTag`, `deleteRemoteTag`, and `cleanupMultiModel` to accept `mode` and `abortSignal` for better control and cancellation.
- Update internal calls to pass the new parameters throughout the cleanup workflow.
- Add detailed JSDoc comments for the new arguments.
- Modify `closeMultiModel` to destructure `mode` and `abortSignal` from options and forward them to `runCommand` calls, including spinner messages for session checks, killing sessions, and worktree removal.
This commit is contained in:
2026-03-23 19:16:12 +05:30
parent 651c2ae9a9
commit ac31d69e3a
6 changed files with 76 additions and 30 deletions
+29 -9
View File
@@ -5,16 +5,32 @@ import { promptUser, runCommand } from "./utils";
/**
* Lists all archive tags matching the pattern archive/*.
*/
export async function listArchiveTags(): Promise<string[]> {
const { stdout } = await runCommand(["git", "tag", "-l", "archive/*"]);
export async function listArchiveTags(
mode?: "cli" | "tool",
abortSignal?: AbortSignal,
): Promise<string[]> {
const { stdout } = await runCommand(["git", "tag", "-l", "archive/*"], {
mode,
abortSignal,
});
return stdout.split("\n").filter(Boolean);
}
/**
* Gets the list of available git remotes.
*/
export async function getAvailableRemotes(): Promise<string[]> {
const { stdout } = await runCommand(["git", "remote"]);
/**
* Retrieves the list of git remotes.
* Accepts optional mode and abort signal for cancellation support.
*/
export async function getAvailableRemotes(
mode?: "cli" | "tool",
abortSignal?: AbortSignal,
): Promise<string[]> {
const { stdout } = await runCommand(["git", "remote"], {
mode,
abortSignal,
});
return stdout.split("\n").filter(Boolean);
}
@@ -24,10 +40,12 @@ export async function getAvailableRemotes(): Promise<string[]> {
async function deleteLocalTag(
tagName: string,
mode?: "cli" | "tool",
abortSignal?: AbortSignal,
): Promise<boolean> {
const result = await runCommand(["git", "tag", "-d", tagName], {
mode,
spinnerMsg: `Deleting local tag ${tagName}...`,
abortSignal,
});
return result.ok;
}
@@ -39,12 +57,14 @@ async function deleteRemoteTag(
tagName: string,
remoteName: string,
mode?: "cli" | "tool",
abortSignal?: AbortSignal,
): Promise<{ ok: boolean; error: string }> {
const result = await runCommand(
["git", "push", remoteName, "--delete", tagName],
{
mode,
spinnerMsg: `Deleting remote tag ${tagName} from ${remoteName}...`,
abortSignal,
},
);
return {
@@ -65,11 +85,11 @@ async function deleteRemoteTag(
export async function cleanupMultiModel(
options: CleanupOptions,
): Promise<CleanupResult> {
const { force = false, remote, mode } = options;
const { force = false, remote, mode, abortSignal } = options;
// Get available remotes and archive tags first
const availableRemotes = await getAvailableRemotes();
const archiveTags = await listArchiveTags();
const availableRemotes = await getAvailableRemotes(mode, abortSignal);
const archiveTags = await listArchiveTags(mode, abortSignal);
// Determine whether to push to remote and which remote to use:
// - remote === false: --no-remote flag, local-only mode
@@ -155,7 +175,7 @@ export async function cleanupMultiModel(
// Delete local tags
for (const tag of archiveTags) {
const deleted = await deleteLocalTag(tag, mode);
const deleted = await deleteLocalTag(tag, mode, abortSignal);
if (deleted) {
localTagNames.push(tag);
}
@@ -164,7 +184,7 @@ export async function cleanupMultiModel(
// Delete from remote (if remote is specified and exists)
if (shouldPushToRemote && remoteToDeleteFrom) {
for (const tag of localTagNames) {
const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode);
const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode, abortSignal);
if (result.ok) {
remoteTagNames.push(tag);
} else {
+6 -4
View File
@@ -36,7 +36,7 @@ export async function closeMultiModel(
const instructionsArr: string[] = [];
const branchesDeleted: string[] = [];
const tagsCreated: string[] = [];
const mode = options.mode;
const {mode , abortSignal} = options;
const safeSessionName = sanitizeName(options.sessionName);
try {
@@ -46,11 +46,11 @@ export async function closeMultiModel(
"has-session",
"-t",
options.sessionName,
]);
], { mode, spinnerMsg: `Checking if session '${options.sessionName}' exists...`, abortSignal });
// Kill tmux session if it exists
if (sessionExists) {
await runCommand(["tmux", "kill-session", "-t", options.sessionName]);
await runCommand(["tmux", "kill-session", "-t", options.sessionName], { mode, spinnerMsg: `Killing session '${options.sessionName}'...`, abortSignal });
instructionsArr.push(
chalk.green(`Closed session '${options.sessionName}'`),
);
@@ -96,6 +96,7 @@ export async function closeMultiModel(
await runCommand(["git", "worktree", "remove", "-f", worktree.path], {
mode,
spinnerMsg: `Removing worktree ${worktree.path}...`,
abortSignal,
});
worktreesRemoved.push(worktree.path);
@@ -121,6 +122,7 @@ export async function closeMultiModel(
await runCommand(["git", "branch", "-D", worktree.branch], {
mode,
spinnerMsg: `Deleting branch ${worktree.branch}...`,
abortSignal,
});
branchesDeleted.push(worktree.branch);
} catch (err) {
@@ -133,7 +135,7 @@ export async function closeMultiModel(
// Also remove the base directory
const worktreeBase = getSessionPath(safeSessionName);
try {
await runCommand(["rm", "-rf", worktreeBase]);
await runCommand(["rm", "-rf", worktreeBase], { mode, spinnerMsg: `Removing worktree base directory: ${worktreeBase}...`, abortSignal });
} catch {
// Ignore errors
}
+31 -9
View File
@@ -42,7 +42,7 @@ export async function openMultiModel(
const safeSessionName = sanitizeName(sessionName);
const models = normalizeModels(options.models);
const binaryName = options.binaryName || "opencode";
const mode = options.mode;
const {mode, abortSignal} = options;
if (!sessionName) {
return {
@@ -73,7 +73,11 @@ export async function openMultiModel(
"git",
"rev-parse",
"--is-inside-work-tree",
]);
], {
mode,
spinnerMsg: "Checking if inside git repository...",
abortSignal,
});
if (!gitRepoCheck.ok) {
return {
success: false,
@@ -82,7 +86,11 @@ export async function openMultiModel(
};
}
const tmuxExists = await runCommand(["command", "-v", "tmux"]);
const tmuxExists = await runCommand(["command", "-v", "tmux"], {
mode,
spinnerMsg: "Checking if tmux is installed...",
abortSignal,
});
if (!tmuxExists.ok) {
return {
success: false,
@@ -91,7 +99,12 @@ export async function openMultiModel(
};
}
const binaryExists = await runCommand(["command", "-v", binaryName]);
const binaryExists = await runCommand(["command", "-v", binaryName], {
mode,
spinnerMsg: `Checking if ${binaryName} is installed...`,
abortSignal,
});
if (!binaryExists.ok) {
return {
success: false,
@@ -103,6 +116,7 @@ export async function openMultiModel(
const modelListResult = await runCommand([binaryName, "models"], {
mode,
spinnerMsg: `Fetching available models using \`${binaryName} models\`...`,
abortSignal,
});
if (!modelListResult.ok) {
return {
@@ -131,7 +145,11 @@ export async function openMultiModel(
"has-session",
"-t",
sessionName,
]);
], {
mode,
spinnerMsg: `Checking if tmux session '${sessionName}' already exists...`,
abortSignal,
});
if (sessionExists.ok) {
return {
success: false,
@@ -172,7 +190,11 @@ export async function openMultiModel(
"--verify",
"--quiet",
`refs/heads/${branchName}`,
]);
], {
mode,
spinnerMsg: `Checking if branch '${branchName}' already exists...`,
abortSignal,
});
if (branchExists.ok) {
if (!isFirst) {
failedModels.push(
@@ -190,7 +212,7 @@ export async function openMultiModel(
const worktreeResult = await runCommand(
["git", "worktree", "add", "-b", branchName, worktreePath],
{ mode, spinnerMsg: `Creating worktree for ${plan.model}...` },
{ mode, spinnerMsg: `Creating worktree for ${plan.model}...`, abortSignal },
);
if (!worktreeResult.ok) {
if (!isFirst) {
@@ -220,7 +242,7 @@ export async function openMultiModel(
"-c",
worktreePath,
],
{ mode, spinnerMsg: `Starting tmux session '${sessionName}'...` },
{ mode, spinnerMsg: `Starting tmux session '${sessionName}'...`, abortSignal },
);
if (!sessionCreateResult.ok) {
@@ -244,7 +266,7 @@ export async function openMultiModel(
"-c",
worktreePath,
],
{ mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...` },
{ mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...`, abortSignal },
);
if (!windowCreateResult.ok) {
+1
View File
@@ -51,6 +51,7 @@ export async function runCommand(
spinner = sharedSpinner;
}
options?.abortSignal?.throwIfAborted();
const result = await Bun.$`${parts}`.quiet().nothrow();
if (spinner) {
+1 -8
View File
@@ -32,14 +32,7 @@ Arguments:
models: args.models,
binaryName,
mode: "tool",
});
context.metadata({
title: `multi-model ${args.sessionName}`,
metadata: {
safeSessionName: result.sessionName,
modelCount: args.models.length,
},
abortSignal: context.abort,
});
if (!result.success) {
+8
View File
@@ -19,6 +19,8 @@ export interface OpenSessionOptions {
mode: "cli" | "tool";
/** Save models to settings file on successful launch (CLI only). */
saveToSettings?: boolean;
/** Signal to abort the operation. */
abortSignal?: AbortSignal;
}
/**
@@ -51,6 +53,8 @@ export interface CloseSessionOptions {
force?: boolean;
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
mode: "cli" | "tool";
/** Signal to abort the operation. */
abortSignal?: AbortSignal;
}
/**
@@ -100,6 +104,8 @@ export interface CleanupOptions {
remote?: string | false;
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
mode: "cli" | "tool";
/** Optional abort signal to cancel the operation. */
abortSignal?: AbortSignal;
}
/**
@@ -146,6 +152,8 @@ export interface RunCommandOptions {
mode?: "cli" | "tool";
/** Message to display in the spinner while the command runs. */
spinnerMsg?: string;
/** Signal to abort the operation. Check it before running the command. */
abortSignal?: AbortSignal;
}
/**