mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 21:52:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16f432c94 | ||
|
|
e4d3e0e263 | ||
|
|
6b07a7ac2a | ||
|
|
415b0624f1 | ||
|
|
4824375970 | ||
|
|
1294fcede1 | ||
|
|
8b85bc4a39 | ||
|
|
78784fabb6 | ||
|
|
c6698e7c22 | ||
|
|
cb9c973868 | ||
|
|
f58cd1e897 | ||
|
|
4e20a03359 |
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
model: mimo-v2-pro-free
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remember Last Used Models
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
When CLI `open` is called without `-m` models, read last used models from `~/.config/opencode-multi-model/settings.json` and confirm with user (Y/n). Use `--force` to skip confirmation. After successful open from CLI, persist models to settings. Tool path is unchanged — always requires explicit values.
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `src/core/utils.ts` | Add `loadSettings()`, `saveSettings()`, and move `promptUser` here |
|
||||||
|
| `src/types.ts` | Add `MultiModelSettings` interface and `saveToSettings` to `MultiModelOptions` |
|
||||||
|
| `src/core/open.ts` | Conditional save after success when `saveToSettings` is true |
|
||||||
|
| `src/core/close.ts` | Remove local `promptUser`, import from utils |
|
||||||
|
| `src/core/cleanup.ts` | Remove local `promptUser`, import from utils |
|
||||||
|
| `src/cli.ts` | Load settings + confirmation (unless `--force`) |
|
||||||
|
| `tests/utils.test.ts` | Add tests for loadSettings/saveSettings |
|
||||||
|
| `tests/open.test.ts` | Add tests for settings save on success |
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. `src/types.ts` — Add settings interface and option flag
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface MultiModelSettings {
|
||||||
|
lastModels: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `saveToSettings?: boolean` to `MultiModelOptions`.
|
||||||
|
|
||||||
|
### 2. `src/core/utils.ts` — Settings read/write + shared promptUser
|
||||||
|
|
||||||
|
Add three new exported functions:
|
||||||
|
|
||||||
|
- `getSettingsPath(): string` — returns `~/.config/opencode-multi-model/settings.json`
|
||||||
|
- `loadSettings(): Promise<MultiModelSettings>` — reads and parses the JSON file. Returns `{ lastModels: [] }` if file doesn't exist or is invalid.
|
||||||
|
- `saveSettings(settings: MultiModelSettings): Promise<void>` — creates directory if needed, writes JSON with 2-space indent.
|
||||||
|
|
||||||
|
Also move the `promptUser` function (currently duplicated in `close.ts:17-29` and `cleanup.ts:9-21`) into `utils.ts` as a shared export. Remove the local copies from both files and update imports.
|
||||||
|
|
||||||
|
### 3. `src/core/open.ts` — Conditional save after full success
|
||||||
|
|
||||||
|
Before the successful return at line 310, if `options.saveToSettings` is true, call `saveSettings({ lastModels: models })`. Only save on full success, not partial. CLI passes `saveToSettings: true`; tool passes nothing (defaults false).
|
||||||
|
|
||||||
|
### 4. `src/cli.ts` — Settings load + confirmation + --force
|
||||||
|
|
||||||
|
Add `-f, --force` option to the `open` command: `.option("-f, --force", "Skip confirmation prompts", false)` (matches close/cleanup pattern).
|
||||||
|
|
||||||
|
In the `open` command action, before calling `openMultiModel`:
|
||||||
|
1. If `options.models` is empty, call `loadSettings()`.
|
||||||
|
2. If `settings.lastModels` is non-empty, use those. If empty, proceed with empty array (existing error from core function).
|
||||||
|
3. If models were loaded from settings and `--force` is not set, confirm: `Use models [model1, model2]? (Y/n): ` using `promptUser` from utils.
|
||||||
|
4. If user rejects (n, no), print error and exit.
|
||||||
|
5. Pass `saveToSettings: true` to `openMultiModel` so successful runs persist the models.
|
||||||
|
|
||||||
|
### 5. Tests
|
||||||
|
|
||||||
|
- `utils.test.ts`: Test `loadSettings` returns defaults for missing file, parses valid JSON, handles corrupt JSON gracefully. Test `saveSettings` writes correct content.
|
||||||
|
- `open.test.ts`: Test that `openMultiModel` calls `saveSettings` on success (mock `fs` / the save function).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Run `bun run typecheck` — no type errors
|
||||||
|
2. Run `bun run lint` — no lint errors
|
||||||
|
3. Run `bun test` — all tests pass
|
||||||
|
4. Manual CLI test:
|
||||||
|
- `opencode-multi-model open test1 -m model1 model2` → saves settings
|
||||||
|
- `opencode-multi-model open test2` → prompts with saved models, confirm with Enter → uses saved models
|
||||||
|
- `opencode-multi-model open test3` → prompt, type `n` → cancels
|
||||||
@@ -81,11 +81,12 @@ bun run typecheck && bun run lint
|
|||||||
- Group imports: external packages, internal modules, types
|
- Group imports: external packages, internal modules, types
|
||||||
- Biome automatically organizes imports
|
- Biome automatically organizes imports
|
||||||
|
|
||||||
### JSDoc Comments
|
### Documentation
|
||||||
|
|
||||||
- Use JSDoc for all exported functions
|
- Use JSDoc for all exported functions
|
||||||
- Include `@example`, `@param`, and `@returns` descriptions
|
- Include `@example`, `@param`, and `@returns` descriptions
|
||||||
- Add inline comments for hard-to-understand code
|
- Add inline comments for hard-to-understand code
|
||||||
|
- Modify Readme.md, if implementation details have changed.
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"@opencode-ai/plugin": "^1.2.27",
|
"@opencode-ai/plugin": "^1.2.27",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
|
"ora": "^9.3.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.8",
|
"@biomejs/biome": "^2.4.8",
|
||||||
@@ -44,16 +45,48 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
|
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||||
|
|
||||||
|
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||||
|
|
||||||
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
|
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
||||||
|
|
||||||
|
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||||
|
|
||||||
|
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||||
|
|
||||||
|
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||||
|
|
||||||
|
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
|
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
|
"ora": ["ora@9.3.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.1", "string-width": "^8.1.0" } }, "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw=="],
|
||||||
|
|
||||||
|
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"stdin-discarder": ["stdin-discarder@0.3.1", "", {}, "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||||
|
|
||||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencode-multi-model",
|
"name": "opencode-multi-model",
|
||||||
"version": "0.1.1",
|
"version": "0.1.5",
|
||||||
"description": "Launch multiple AI models in tmux sessions - OpenCode Plugin, CLI, and Project Tool",
|
"description": "Launch multiple AI models in tmux sessions - OpenCode Plugin, CLI, and Project Tool",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -29,9 +29,10 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"@opencode-ai/plugin": "^1.2.27",
|
"@opencode-ai/plugin": "^1.2.27",
|
||||||
"commander": "^12.0.0"
|
"chalk": "^5.3.0",
|
||||||
|
"commander": "^12.0.0",
|
||||||
|
"ora": "^9.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.8",
|
"@biomejs/biome": "^2.4.8",
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
import pkg from "../package.json";
|
||||||
import { cleanupMultiModel } from "./core/cleanup";
|
import { cleanupMultiModel } from "./core/cleanup";
|
||||||
import { closeMultiModel } from "./core/close";
|
import { closeMultiModel } from "./core/close";
|
||||||
import { openMultiModel } from "./core/open";
|
import { openMultiModel } from "./core/open";
|
||||||
@@ -10,7 +11,7 @@ const program = new Command();
|
|||||||
program
|
program
|
||||||
.name("opencode-multi-model")
|
.name("opencode-multi-model")
|
||||||
.description("Launch multiple OpenCode models in tmux sessions")
|
.description("Launch multiple OpenCode models in tmux sessions")
|
||||||
.version("1.0.0");
|
.version(pkg.version);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("open")
|
.command("open")
|
||||||
@@ -77,6 +78,7 @@ program
|
|||||||
cleanupWorktrees: !options.keepWorktrees,
|
cleanupWorktrees: !options.keepWorktrees,
|
||||||
force: options.force,
|
force: options.force,
|
||||||
createArchiveTags: options.tags,
|
createArchiveTags: options.tags,
|
||||||
|
mode: "cli",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -106,6 +108,7 @@ program
|
|||||||
const result = await cleanupMultiModel({
|
const result = await cleanupMultiModel({
|
||||||
force: options.force,
|
force: options.force,
|
||||||
remote: options.remote === true ? undefined : options.remote,
|
remote: options.remote === true ? undefined : options.remote,
|
||||||
|
mode: "cli",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
+20
-13
@@ -38,8 +38,14 @@ export async function getAvailableRemotes(): Promise<string[]> {
|
|||||||
/**
|
/**
|
||||||
* Deletes a local git tag.
|
* Deletes a local git tag.
|
||||||
*/
|
*/
|
||||||
async function deleteLocalTag(tagName: string): Promise<boolean> {
|
async function deleteLocalTag(
|
||||||
const result = await runCommand(["git", "tag", "-d", tagName]);
|
tagName: string,
|
||||||
|
mode?: "cli" | "tool",
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await runCommand(["git", "tag", "-d", tagName], {
|
||||||
|
mode,
|
||||||
|
spinnerMsg: `Deleting local tag ${tagName}...`,
|
||||||
|
});
|
||||||
return result.ok;
|
return result.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,14 +55,15 @@ async function deleteLocalTag(tagName: string): Promise<boolean> {
|
|||||||
async function deleteRemoteTag(
|
async function deleteRemoteTag(
|
||||||
tagName: string,
|
tagName: string,
|
||||||
remoteName: string,
|
remoteName: string,
|
||||||
|
mode?: "cli" | "tool",
|
||||||
): Promise<{ ok: boolean; error: string }> {
|
): Promise<{ ok: boolean; error: string }> {
|
||||||
const result = await runCommand([
|
const result = await runCommand(
|
||||||
"git",
|
["git", "push", remoteName, "--delete", tagName],
|
||||||
"push",
|
{
|
||||||
remoteName,
|
mode,
|
||||||
"--delete",
|
spinnerMsg: `Deleting remote tag ${tagName} from ${remoteName}...`,
|
||||||
tagName,
|
},
|
||||||
]);
|
);
|
||||||
return {
|
return {
|
||||||
ok: result.ok,
|
ok: result.ok,
|
||||||
error: result.ok ? "" : result.stderr,
|
error: result.ok ? "" : result.stderr,
|
||||||
@@ -73,9 +80,9 @@ async function deleteRemoteTag(
|
|||||||
* @returns Result with counts and details of deleted tags.
|
* @returns Result with counts and details of deleted tags.
|
||||||
*/
|
*/
|
||||||
export async function cleanupMultiModel(
|
export async function cleanupMultiModel(
|
||||||
options: CleanupOptions = {},
|
options: CleanupOptions,
|
||||||
): Promise<CleanupResult> {
|
): Promise<CleanupResult> {
|
||||||
const { force = false, remote } = options;
|
const { force = false, remote, mode } = options;
|
||||||
|
|
||||||
// Get available remotes and archive tags first
|
// Get available remotes and archive tags first
|
||||||
const availableRemotes = await getAvailableRemotes();
|
const availableRemotes = await getAvailableRemotes();
|
||||||
@@ -165,7 +172,7 @@ export async function cleanupMultiModel(
|
|||||||
|
|
||||||
// Delete local tags
|
// Delete local tags
|
||||||
for (const tag of archiveTags) {
|
for (const tag of archiveTags) {
|
||||||
const deleted = await deleteLocalTag(tag);
|
const deleted = await deleteLocalTag(tag, mode);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
localTagNames.push(tag);
|
localTagNames.push(tag);
|
||||||
}
|
}
|
||||||
@@ -174,7 +181,7 @@ export async function cleanupMultiModel(
|
|||||||
// Delete from remote (if remote is specified and exists)
|
// Delete from remote (if remote is specified and exists)
|
||||||
if (shouldPushToRemote && remoteToDeleteFrom) {
|
if (shouldPushToRemote && remoteToDeleteFrom) {
|
||||||
for (const tag of localTagNames) {
|
for (const tag of localTagNames) {
|
||||||
const result = await deleteRemoteTag(tag, remoteToDeleteFrom);
|
const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
remoteTagNames.push(tag);
|
remoteTagNames.push(tag);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+13
-8
@@ -56,6 +56,7 @@ export async function closeMultiModel(
|
|||||||
const instructionsArr: string[] = [];
|
const instructionsArr: string[] = [];
|
||||||
const branchesDeleted: string[] = [];
|
const branchesDeleted: string[] = [];
|
||||||
const tagsCreated: string[] = [];
|
const tagsCreated: string[] = [];
|
||||||
|
const mode = options.mode;
|
||||||
|
|
||||||
const safeSessionName = sanitizeName(options.sessionName);
|
const safeSessionName = sanitizeName(options.sessionName);
|
||||||
try {
|
try {
|
||||||
@@ -112,19 +113,20 @@ export async function closeMultiModel(
|
|||||||
for (const worktree of worktrees) {
|
for (const worktree of worktrees) {
|
||||||
try {
|
try {
|
||||||
// Remove worktree
|
// 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);
|
worktreesRemoved.push(worktree.path);
|
||||||
|
|
||||||
// Optionally create an archive tag before deleting the branch for safe recovery
|
// Optionally create an archive tag before deleting the branch for safe recovery
|
||||||
if (options.createArchiveTags !== false) {
|
if (options.createArchiveTags !== false) {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
const archiveTag = `archive/${worktree.branch}-${timestamp}`;
|
const archiveTag = `archive/${worktree.branch}-${timestamp}`;
|
||||||
const tagResult = await runCommand([
|
const tagResult = await runCommand(
|
||||||
"git",
|
["git", "tag", archiveTag, worktree.branch],
|
||||||
"tag",
|
{ mode, spinnerMsg: `Creating archive tag ${archiveTag}...` },
|
||||||
archiveTag,
|
);
|
||||||
worktree.branch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (tagResult.ok) {
|
if (tagResult.ok) {
|
||||||
tagsCreated.push(archiveTag);
|
tagsCreated.push(archiveTag);
|
||||||
@@ -136,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);
|
branchesDeleted.push(worktree.branch);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
instructionsArr.push(
|
instructionsArr.push(
|
||||||
|
|||||||
+38
-32
@@ -41,7 +41,7 @@ export async function openMultiModel(
|
|||||||
const safeSessionName = sanitizeName(sessionName);
|
const safeSessionName = sanitizeName(sessionName);
|
||||||
const models = normalizeModels(options.models);
|
const models = normalizeModels(options.models);
|
||||||
const binaryName = options.binaryName || "opencode";
|
const binaryName = options.binaryName || "opencode";
|
||||||
const mode = options.mode || "cli";
|
const mode = options.mode;
|
||||||
|
|
||||||
if (!sessionName) {
|
if (!sessionName) {
|
||||||
return {
|
return {
|
||||||
@@ -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) {
|
if (!modelListResult.ok) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -184,14 +187,10 @@ export async function openMultiModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const worktreeResult = await runCommand([
|
const worktreeResult = await runCommand(
|
||||||
"git",
|
["git", "worktree", "add", "-b", branchName, worktreePath],
|
||||||
"worktree",
|
{ mode, spinnerMsg: `Creating worktree for ${plan.model}...` },
|
||||||
"add",
|
);
|
||||||
"-b",
|
|
||||||
branchName,
|
|
||||||
worktreePath,
|
|
||||||
]);
|
|
||||||
if (!worktreeResult.ok) {
|
if (!worktreeResult.ok) {
|
||||||
if (!isFirst) {
|
if (!isFirst) {
|
||||||
failedModels.push(
|
failedModels.push(
|
||||||
@@ -208,17 +207,20 @@ export async function openMultiModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
const sessionCreateResult = await runCommand([
|
const sessionCreateResult = await runCommand(
|
||||||
"tmux",
|
[
|
||||||
"new-session",
|
"tmux",
|
||||||
"-d",
|
"new-session",
|
||||||
"-s",
|
"-d",
|
||||||
sessionName,
|
"-s",
|
||||||
"-n",
|
sessionName,
|
||||||
plan.windowName,
|
"-n",
|
||||||
"-c",
|
plan.windowName,
|
||||||
worktreePath,
|
"-c",
|
||||||
]);
|
worktreePath,
|
||||||
|
],
|
||||||
|
{ mode, spinnerMsg: `Starting tmux session '${sessionName}'...` },
|
||||||
|
);
|
||||||
|
|
||||||
if (!sessionCreateResult.ok) {
|
if (!sessionCreateResult.ok) {
|
||||||
const undoErrors = await undoWorktree(worktreePath, branchName);
|
const undoErrors = await undoWorktree(worktreePath, branchName);
|
||||||
@@ -229,17 +231,20 @@ export async function openMultiModel(
|
|||||||
return { success: false, sessionName, error: errorMsg };
|
return { success: false, sessionName, error: errorMsg };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const windowCreateResult = await runCommand([
|
const windowCreateResult = await runCommand(
|
||||||
"tmux",
|
[
|
||||||
"new-window",
|
"tmux",
|
||||||
"-d",
|
"new-window",
|
||||||
"-t",
|
"-d",
|
||||||
sessionName,
|
"-t",
|
||||||
"-n",
|
sessionName,
|
||||||
plan.windowName,
|
"-n",
|
||||||
"-c",
|
plan.windowName,
|
||||||
worktreePath,
|
"-c",
|
||||||
]);
|
worktreePath,
|
||||||
|
],
|
||||||
|
{ mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...` },
|
||||||
|
);
|
||||||
|
|
||||||
if (!windowCreateResult.ok) {
|
if (!windowCreateResult.ok) {
|
||||||
failedModels.push(
|
failedModels.push(
|
||||||
@@ -259,6 +264,7 @@ export async function openMultiModel(
|
|||||||
sessionName,
|
sessionName,
|
||||||
plan,
|
plan,
|
||||||
binaryName,
|
binaryName,
|
||||||
|
mode,
|
||||||
);
|
);
|
||||||
if (launchResult.ok) {
|
if (launchResult.ok) {
|
||||||
succeededModels.push(plan.model);
|
succeededModels.push(plan.model);
|
||||||
|
|||||||
+40
-10
@@ -1,6 +1,12 @@
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
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 MAX_SUGGESTIONS = 3;
|
||||||
const WINDOW_NAME_LIMIT = 24;
|
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.
|
* Runs a command and captures its output without throwing on non-zero exit codes.
|
||||||
*
|
*
|
||||||
* @param parts Command segments to pass to the shell.
|
* @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.
|
* @returns The exit status plus captured stdout and stderr.
|
||||||
*
|
*
|
||||||
* @example
|
* @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();
|
const result = await Bun.$`${parts}`.quiet().nothrow();
|
||||||
|
|
||||||
|
if (spinner) {
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
spinner.succeed();
|
||||||
|
} else {
|
||||||
|
spinner.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: result.exitCode === 0,
|
ok: result.exitCode === 0,
|
||||||
stdout: result.stdout.toString().trim(),
|
stdout: result.stdout.toString().trim(),
|
||||||
@@ -274,17 +300,21 @@ export async function launchModelInWindow(
|
|||||||
sessionName: string,
|
sessionName: string,
|
||||||
plan: WindowLaunchPlan,
|
plan: WindowLaunchPlan,
|
||||||
binaryName: string = "opencode",
|
binaryName: string = "opencode",
|
||||||
|
mode?: "cli" | "tool",
|
||||||
): Promise<CommandResult> {
|
): Promise<CommandResult> {
|
||||||
const launchCommand = `${binaryName} --model ${shellQuote(plan.model)}`;
|
const launchCommand = `${binaryName} --model ${shellQuote(plan.model)}`;
|
||||||
|
|
||||||
return runCommand([
|
return runCommand(
|
||||||
"tmux",
|
[
|
||||||
"send-keys",
|
"tmux",
|
||||||
"-t",
|
"send-keys",
|
||||||
`${sessionName}:${plan.windowName}`,
|
"-t",
|
||||||
launchCommand,
|
`${sessionName}:${plan.windowName}`,
|
||||||
"C-m",
|
launchCommand,
|
||||||
]);
|
"C-m",
|
||||||
|
],
|
||||||
|
{ mode, spinnerMsg: `Launching model ${plan.model}...` },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,19 +15,14 @@ Arguments:
|
|||||||
- \`noRemote\` (boolean, optional): When true, only delete local tags and do not push deletions to any remote.`,
|
- \`noRemote\` (boolean, optional): When true, only delete local tags and do not push deletions to any remote.`,
|
||||||
args: {
|
args: {
|
||||||
remote: tool.schema
|
remote: tool.schema
|
||||||
.union([
|
.string()
|
||||||
tool.schema
|
|
||||||
.string()
|
|
||||||
.describe(
|
|
||||||
"Remote name to push deletions to. Error if doesn't exist.",
|
|
||||||
)
|
|
||||||
])
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Remote handling: string=specific remote, undefined=first available remote",
|
"Remote handling: string=specific remote, undefined=first available remote",
|
||||||
),
|
),
|
||||||
noRemote: tool.schema
|
noRemote: tool.schema
|
||||||
.literal(true)
|
.boolean()
|
||||||
|
.default(false)
|
||||||
.describe("Only delete local tags, don't push to remote.")
|
.describe("Only delete local tags, don't push to remote.")
|
||||||
.optional(),
|
.optional(),
|
||||||
},
|
},
|
||||||
@@ -36,6 +31,7 @@ Arguments:
|
|||||||
const result = await cleanupMultiModel({
|
const result = await cleanupMultiModel({
|
||||||
force: true,
|
force: true,
|
||||||
remote: args.noRemote ? false : args.remote,
|
remote: args.noRemote ? false : args.remote,
|
||||||
|
mode: "tool",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Arguments:
|
|||||||
cleanupWorktrees: args.cleanupWorktrees,
|
cleanupWorktrees: args.cleanupWorktrees,
|
||||||
createArchiveTags: args.createArchiveTags,
|
createArchiveTags: args.createArchiveTags,
|
||||||
force: true,
|
force: true,
|
||||||
|
mode: "tool",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
+15
-1
@@ -9,7 +9,7 @@ export interface MultiModelOptions {
|
|||||||
/** Binary name to use ('opencode' or 'kilo'). Defaults to 'opencode'. */
|
/** Binary name to use ('opencode' or 'kilo'). Defaults to 'opencode'. */
|
||||||
binaryName?: string;
|
binaryName?: string;
|
||||||
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
|
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
|
||||||
mode?: "cli" | "tool";
|
mode: "cli" | "tool";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +40,8 @@ export interface CloseSessionOptions {
|
|||||||
cleanupWorktrees?: boolean;
|
cleanupWorktrees?: boolean;
|
||||||
/** Skip confirmation prompts. */
|
/** Skip confirmation prompts. */
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
|
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
|
||||||
|
mode: "cli" | "tool";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +89,8 @@ export interface CleanupOptions {
|
|||||||
* - If false: only local cleanup (--no-remote flag)
|
* - If false: only local cleanup (--no-remote flag)
|
||||||
*/
|
*/
|
||||||
remote?: string | false;
|
remote?: string | false;
|
||||||
|
/** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */
|
||||||
|
mode: "cli" | "tool";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +129,16 @@ export interface CommandResult {
|
|||||||
stderr: string;
|
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.
|
* Information about a git worktree.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+19
-9
@@ -70,7 +70,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true });
|
const result = await cleanupMultiModel({ mode: "cli", force: true });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(0);
|
expect(result.localTagsDeleted).toBe(0);
|
||||||
@@ -96,7 +96,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: false });
|
const result = await cleanupMultiModel({ mode: "cli", force: false });
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.error).toBe("Cleanup cancelled by user");
|
expect(result.error).toBe("Cleanup cancelled by user");
|
||||||
@@ -126,7 +126,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true });
|
const result = await cleanupMultiModel({ mode: "cli", force: true });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(1);
|
expect(result.localTagsDeleted).toBe(1);
|
||||||
@@ -150,7 +150,11 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true, remote: false });
|
const result = await cleanupMultiModel({
|
||||||
|
mode: "cli",
|
||||||
|
force: true,
|
||||||
|
remote: false,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(1);
|
expect(result.localTagsDeleted).toBe(1);
|
||||||
@@ -191,7 +195,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true });
|
const result = await cleanupMultiModel({ mode: "cli", force: true });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(2);
|
expect(result.localTagsDeleted).toBe(2);
|
||||||
@@ -222,7 +226,11 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true, remote: undefined });
|
const result = await cleanupMultiModel({
|
||||||
|
mode: "cli",
|
||||||
|
force: true,
|
||||||
|
remote: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(executedCommands).toContain(
|
expect(executedCommands).toContain(
|
||||||
@@ -253,6 +261,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({
|
const result = await cleanupMultiModel({
|
||||||
|
mode: "cli",
|
||||||
force: true,
|
force: true,
|
||||||
remote: "upstream",
|
remote: "upstream",
|
||||||
});
|
});
|
||||||
@@ -277,6 +286,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({
|
const result = await cleanupMultiModel({
|
||||||
|
mode: "cli",
|
||||||
force: true,
|
force: true,
|
||||||
remote: "nonexistent",
|
remote: "nonexistent",
|
||||||
});
|
});
|
||||||
@@ -303,7 +313,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true });
|
const result = await cleanupMultiModel({ mode: "cli", force: true });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.availableRemotes).toEqual([]);
|
expect(result.availableRemotes).toEqual([]);
|
||||||
@@ -339,7 +349,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: false });
|
const result = await cleanupMultiModel({ mode: "cli", force: false });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(1);
|
expect(result.localTagsDeleted).toBe(1);
|
||||||
@@ -392,7 +402,7 @@ describe("cleanupMultiModel", () => {
|
|||||||
stderr: "",
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await cleanupMultiModel({ force: true });
|
const result = await cleanupMultiModel({ mode: "cli", force: true });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localTagsDeleted).toBe(3);
|
expect(result.localTagsDeleted).toBe(3);
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ describe("closeMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: false,
|
cleanupWorktrees: false,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -103,6 +104,7 @@ describe("closeMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: false,
|
cleanupWorktrees: false,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -161,6 +163,7 @@ describe("closeMultiModel", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: true,
|
force: true,
|
||||||
@@ -206,6 +209,7 @@ describe("closeMultiModel", () => {
|
|||||||
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -251,6 +255,7 @@ describe("closeMultiModel", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -322,6 +327,7 @@ describe("closeMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: true,
|
force: true,
|
||||||
@@ -380,6 +386,7 @@ describe("closeMultiModel", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: unsanitized,
|
sessionName: unsanitized,
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: true,
|
force: true,
|
||||||
@@ -422,6 +429,7 @@ describe("closeMultiModel", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: unsanitized,
|
sessionName: unsanitized,
|
||||||
cleanupWorktrees: false,
|
cleanupWorktrees: false,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -465,6 +473,7 @@ describe("closeMultiModel", () => {
|
|||||||
] = { ok: true, stdout: "", stderr: "" };
|
] = { ok: true, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
cleanupWorktrees: true,
|
cleanupWorktrees: true,
|
||||||
force: true,
|
force: true,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ describe("closeMultiModel unexpected error handling", () => {
|
|||||||
|
|
||||||
test("catches thrown error and returns failure", async () => {
|
test("catches thrown error and returns failure", async () => {
|
||||||
const result = await closeMultiModel({
|
const result = await closeMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "any-session",
|
sessionName: "any-session",
|
||||||
cleanupWorktrees: false,
|
cleanupWorktrees: false,
|
||||||
force: false,
|
force: false,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ describe("multi-model launch", () => {
|
|||||||
|
|
||||||
test("fails if session name is empty", async () => {
|
test("fails if session name is empty", async () => {
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "",
|
sessionName: "",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -105,6 +106,7 @@ describe("multi-model launch", () => {
|
|||||||
|
|
||||||
test("fails if no models are provided", async () => {
|
test("fails if no models are provided", async () => {
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: [],
|
models: [],
|
||||||
});
|
});
|
||||||
@@ -116,6 +118,7 @@ describe("multi-model launch", () => {
|
|||||||
|
|
||||||
test("fails if duplicate models are provided", async () => {
|
test("fails if duplicate models are provided", async () => {
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2", "openai/gpt-5.2"],
|
models: ["openai/gpt-5.2", "openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -133,6 +136,7 @@ describe("multi-model launch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -152,6 +156,7 @@ describe("multi-model launch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -174,6 +179,7 @@ describe("multi-model launch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -191,6 +197,7 @@ describe("multi-model launch", () => {
|
|||||||
|
|
||||||
test("fails if model name is not in allowlist", async () => {
|
test("fails if model name is not in allowlist", async () => {
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["invalid/model"],
|
models: ["invalid/model"],
|
||||||
});
|
});
|
||||||
@@ -215,6 +222,7 @@ describe("multi-model launch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -232,6 +240,7 @@ describe("multi-model launch", () => {
|
|||||||
|
|
||||||
test("successfully plans and executes tmux launch with single model", async () => {
|
test("successfully plans and executes tmux launch with single model", async () => {
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -258,6 +267,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: false, stdout: "", stderr: "" };
|
] = { ok: false, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2", "anthropic/claude-3-5-sonnet"],
|
models: ["openai/gpt-5.2", "anthropic/claude-3-5-sonnet"],
|
||||||
});
|
});
|
||||||
@@ -289,6 +299,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: false, stdout: "", stderr: "" };
|
] = { ok: false, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test session!",
|
sessionName: "test session!",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -326,6 +337,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: true, stdout: "", stderr: "" };
|
] = { ok: true, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: unsanitized,
|
sessionName: unsanitized,
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -373,6 +385,7 @@ describe("multi-model launch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -399,6 +412,7 @@ describe("multi-model launch", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -420,6 +434,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: true, stdout: "", stderr: "" };
|
] = { ok: true, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -444,6 +459,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: false, stdout: "", stderr: "git error" };
|
] = { ok: false, stdout: "", stderr: "git error" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2"],
|
models: ["openai/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -475,6 +491,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: false, stdout: "", stderr: "" };
|
] = { ok: false, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: ["openai/gpt-5.2", "anthropic/gpt-5.2"],
|
models: ["openai/gpt-5.2", "anthropic/gpt-5.2"],
|
||||||
});
|
});
|
||||||
@@ -507,6 +524,7 @@ describe("multi-model launch", () => {
|
|||||||
] = { ok: false, stdout: "", stderr: "" };
|
] = { ok: false, stdout: "", stderr: "" };
|
||||||
|
|
||||||
const result = await openMultiModel({
|
const result = await openMultiModel({
|
||||||
|
mode: "cli",
|
||||||
sessionName: "test-session",
|
sessionName: "test-session",
|
||||||
models: [longModel],
|
models: [longModel],
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,9 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"types": ["bun-types"]
|
"types": ["bun-types"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "tests/**/*"],
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Reference in New Issue
Block a user