mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
feat: add ora spinner (v9.3.0) while running shell
This commit is contained in:
+18
-11
@@ -38,8 +38,14 @@ export async function getAvailableRemotes(): Promise<string[]> {
|
||||
/**
|
||||
* Deletes a local git tag.
|
||||
*/
|
||||
async function deleteLocalTag(tagName: string): Promise<boolean> {
|
||||
const result = await runCommand(["git", "tag", "-d", tagName]);
|
||||
async function deleteLocalTag(
|
||||
tagName: string,
|
||||
mode?: "cli" | "tool",
|
||||
): Promise<boolean> {
|
||||
const result = await runCommand(["git", "tag", "-d", tagName], {
|
||||
mode,
|
||||
spinnerMsg: `Deleting local tag ${tagName}...`,
|
||||
});
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
@@ -49,14 +55,15 @@ async function deleteLocalTag(tagName: string): Promise<boolean> {
|
||||
async function deleteRemoteTag(
|
||||
tagName: string,
|
||||
remoteName: string,
|
||||
mode?: "cli" | "tool",
|
||||
): Promise<{ ok: boolean; error: string }> {
|
||||
const result = await runCommand([
|
||||
"git",
|
||||
"push",
|
||||
remoteName,
|
||||
"--delete",
|
||||
tagName,
|
||||
]);
|
||||
const result = await runCommand(
|
||||
["git", "push", remoteName, "--delete", tagName],
|
||||
{
|
||||
mode,
|
||||
spinnerMsg: `Deleting remote tag ${tagName} from ${remoteName}...`,
|
||||
},
|
||||
);
|
||||
return {
|
||||
ok: result.ok,
|
||||
error: result.ok ? "" : result.stderr,
|
||||
@@ -165,7 +172,7 @@ export async function cleanupMultiModel(
|
||||
|
||||
// Delete local tags
|
||||
for (const tag of archiveTags) {
|
||||
const deleted = await deleteLocalTag(tag);
|
||||
const deleted = await deleteLocalTag(tag, mode);
|
||||
if (deleted) {
|
||||
localTagNames.push(tag);
|
||||
}
|
||||
@@ -174,7 +181,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);
|
||||
const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode);
|
||||
if (result.ok) {
|
||||
remoteTagNames.push(tag);
|
||||
} else {
|
||||
|
||||
+12
-8
@@ -113,19 +113,20 @@ export async function closeMultiModel(
|
||||
for (const worktree of worktrees) {
|
||||
try {
|
||||
// Remove worktree
|
||||
await runCommand(["git", "worktree", "remove", "-f", worktree.path]);
|
||||
await runCommand(["git", "worktree", "remove", "-f", worktree.path], {
|
||||
mode,
|
||||
spinnerMsg: `Removing worktree ${worktree.path}...`,
|
||||
});
|
||||
worktreesRemoved.push(worktree.path);
|
||||
|
||||
// Optionally create an archive tag before deleting the branch for safe recovery
|
||||
if (options.createArchiveTags !== false) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const archiveTag = `archive/${worktree.branch}-${timestamp}`;
|
||||
const tagResult = await runCommand([
|
||||
"git",
|
||||
"tag",
|
||||
archiveTag,
|
||||
worktree.branch,
|
||||
]);
|
||||
const tagResult = await runCommand(
|
||||
["git", "tag", archiveTag, worktree.branch],
|
||||
{ mode, spinnerMsg: `Creating archive tag ${archiveTag}...` },
|
||||
);
|
||||
|
||||
if (tagResult.ok) {
|
||||
tagsCreated.push(archiveTag);
|
||||
@@ -137,7 +138,10 @@ export async function closeMultiModel(
|
||||
}
|
||||
}
|
||||
|
||||
await runCommand(["git", "branch", "-D", worktree.branch]);
|
||||
await runCommand(["git", "branch", "-D", worktree.branch], {
|
||||
mode,
|
||||
spinnerMsg: `Deleting branch ${worktree.branch}...`,
|
||||
});
|
||||
branchesDeleted.push(worktree.branch);
|
||||
} catch (err) {
|
||||
instructionsArr.push(
|
||||
|
||||
+37
-31
@@ -99,7 +99,10 @@ export async function openMultiModel(
|
||||
};
|
||||
}
|
||||
|
||||
const modelListResult = await runCommand([binaryName, "models"]);
|
||||
const modelListResult = await runCommand([binaryName, "models"], {
|
||||
mode,
|
||||
spinnerMsg: `Fetching available models using \`${binaryName} models\`...`,
|
||||
});
|
||||
if (!modelListResult.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -184,14 +187,10 @@ export async function openMultiModel(
|
||||
}
|
||||
}
|
||||
|
||||
const worktreeResult = await runCommand([
|
||||
"git",
|
||||
"worktree",
|
||||
"add",
|
||||
"-b",
|
||||
branchName,
|
||||
worktreePath,
|
||||
]);
|
||||
const worktreeResult = await runCommand(
|
||||
["git", "worktree", "add", "-b", branchName, worktreePath],
|
||||
{ mode, spinnerMsg: `Creating worktree for ${plan.model}...` },
|
||||
);
|
||||
if (!worktreeResult.ok) {
|
||||
if (!isFirst) {
|
||||
failedModels.push(
|
||||
@@ -208,17 +207,20 @@ export async function openMultiModel(
|
||||
}
|
||||
|
||||
if (isFirst) {
|
||||
const sessionCreateResult = await runCommand([
|
||||
"tmux",
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
sessionName,
|
||||
"-n",
|
||||
plan.windowName,
|
||||
"-c",
|
||||
worktreePath,
|
||||
]);
|
||||
const sessionCreateResult = await runCommand(
|
||||
[
|
||||
"tmux",
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
sessionName,
|
||||
"-n",
|
||||
plan.windowName,
|
||||
"-c",
|
||||
worktreePath,
|
||||
],
|
||||
{ mode, spinnerMsg: `Starting tmux session '${sessionName}'...` },
|
||||
);
|
||||
|
||||
if (!sessionCreateResult.ok) {
|
||||
const undoErrors = await undoWorktree(worktreePath, branchName);
|
||||
@@ -229,17 +231,20 @@ export async function openMultiModel(
|
||||
return { success: false, sessionName, error: errorMsg };
|
||||
}
|
||||
} else {
|
||||
const windowCreateResult = await runCommand([
|
||||
"tmux",
|
||||
"new-window",
|
||||
"-d",
|
||||
"-t",
|
||||
sessionName,
|
||||
"-n",
|
||||
plan.windowName,
|
||||
"-c",
|
||||
worktreePath,
|
||||
]);
|
||||
const windowCreateResult = await runCommand(
|
||||
[
|
||||
"tmux",
|
||||
"new-window",
|
||||
"-d",
|
||||
"-t",
|
||||
sessionName,
|
||||
"-n",
|
||||
plan.windowName,
|
||||
"-c",
|
||||
worktreePath,
|
||||
],
|
||||
{ mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...` },
|
||||
);
|
||||
|
||||
if (!windowCreateResult.ok) {
|
||||
failedModels.push(
|
||||
@@ -259,6 +264,7 @@ export async function openMultiModel(
|
||||
sessionName,
|
||||
plan,
|
||||
binaryName,
|
||||
mode,
|
||||
);
|
||||
if (launchResult.ok) {
|
||||
succeededModels.push(plan.model);
|
||||
|
||||
+40
-10
@@ -1,6 +1,12 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { CommandResult, WindowLaunchPlan, WorktreeInfo } from "../types";
|
||||
import ora, { type Ora } from "ora";
|
||||
import type {
|
||||
CommandResult,
|
||||
RunCommandOptions,
|
||||
WindowLaunchPlan,
|
||||
WorktreeInfo,
|
||||
} from "../types";
|
||||
|
||||
const MAX_SUGGESTIONS = 3;
|
||||
const WINDOW_NAME_LIMIT = 24;
|
||||
@@ -9,6 +15,7 @@ const WINDOW_NAME_LIMIT = 24;
|
||||
* Runs a command and captures its output without throwing on non-zero exit codes.
|
||||
*
|
||||
* @param parts Command segments to pass to the shell.
|
||||
* @param options Additional options for running the command (e.g., mode, spinnerMsg).
|
||||
* @returns The exit status plus captured stdout and stderr.
|
||||
*
|
||||
* @example
|
||||
@@ -19,9 +26,28 @@ const WINDOW_NAME_LIMIT = 24;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function runCommand(parts: string[]): Promise<CommandResult> {
|
||||
export async function runCommand(
|
||||
parts: string[],
|
||||
options?: RunCommandOptions,
|
||||
): Promise<CommandResult> {
|
||||
let spinner: Ora | undefined;
|
||||
const isTest =
|
||||
process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test";
|
||||
if (options?.mode === "cli" && !isTest) {
|
||||
const msg = options.spinnerMsg || `Running ${parts[0]}...`;
|
||||
spinner = ora(msg).start();
|
||||
}
|
||||
|
||||
const result = await Bun.$`${parts}`.quiet().nothrow();
|
||||
|
||||
if (spinner) {
|
||||
if (result.exitCode === 0) {
|
||||
spinner.succeed();
|
||||
} else {
|
||||
spinner.fail();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: result.exitCode === 0,
|
||||
stdout: result.stdout.toString().trim(),
|
||||
@@ -274,17 +300,21 @@ export async function launchModelInWindow(
|
||||
sessionName: string,
|
||||
plan: WindowLaunchPlan,
|
||||
binaryName: string = "opencode",
|
||||
mode?: "cli" | "tool",
|
||||
): Promise<CommandResult> {
|
||||
const launchCommand = `${binaryName} --model ${shellQuote(plan.model)}`;
|
||||
|
||||
return runCommand([
|
||||
"tmux",
|
||||
"send-keys",
|
||||
"-t",
|
||||
`${sessionName}:${plan.windowName}`,
|
||||
launchCommand,
|
||||
"C-m",
|
||||
]);
|
||||
return runCommand(
|
||||
[
|
||||
"tmux",
|
||||
"send-keys",
|
||||
"-t",
|
||||
`${sessionName}:${plan.windowName}`,
|
||||
launchCommand,
|
||||
"C-m",
|
||||
],
|
||||
{ mode, spinnerMsg: `Launching model ${plan.model}...` },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -129,6 +129,16 @@ export interface CommandResult {
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for running a shell command.
|
||||
*/
|
||||
export interface RunCommandOptions {
|
||||
/** Caller context: 'cli' shows spinner, 'tool' does not. */
|
||||
mode?: "cli" | "tool";
|
||||
/** Message to display in the spinner while the command runs. */
|
||||
spinnerMsg?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a git worktree.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user