mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
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:
+29
-9
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -51,6 +51,7 @@ export async function runCommand(
|
||||
spinner = sharedSpinner;
|
||||
}
|
||||
|
||||
options?.abortSignal?.throwIfAborted();
|
||||
const result = await Bun.$`${parts}`.quiet().nothrow();
|
||||
|
||||
if (spinner) {
|
||||
|
||||
+1
-8
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user