feat(cli): setup-roo — configure Roo Code for OmniRoute (import JSON + autoImport + UI) (#4292)

CLI #8 of the series. Roo Code (RooVeterinaryInc.roo-cline, a Cline fork) keeps
live settings in opaque VS Code globalStorage, but supports Settings Import and
an `roo-cline.autoImportSettingsPath` (VS Code settings.json) that loads a JSON
at startup.

`omniroute setup-roo`:
- writes ~/.omniroute/roo-settings.json — a Roo provider profile
  (providerProfiles.apiConfigs.OmniRoute: apiProvider=openai, openAiBaseUrl WITH
  /v1 — Roo appends /chat/completions — openAiApiKey, openAiModelId).
- sets roo-cline.autoImportSettingsPath in VS Code settings.json when present
  (preserves other settings).
- prints the guaranteed UI path (Settings → Providers → OpenAI Compatible) +
  the "Import Settings" fallback.
- remote-aware; model via --model or interactive pick.

Researched against current Roo docs: OpenAI-compatible needs baseUrl WITH /v1 and
native tool-calling (OmniRoute supports it). Roo's wire (/v1/chat/completions)
already validated → "OK".

Tests: resolveRooTarget (/v1, key), buildRooImport (provider profile + /v1 + key
fallback), buildRooVscodeAutoImport (pointer + preserve). 5 unit tests; cli-i18n green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-19 16:13:26 -03:00
committed by GitHub
parent 9616b65b53
commit 5b40069b71
4 changed files with 201 additions and 0 deletions

View File

@@ -64,6 +64,7 @@ import { registerSetupCline } from "./setup-cline.mjs";
import { registerSetupKilo } from "./setup-kilo.mjs";
import { registerSetupContinue } from "./setup-continue.mjs";
import { registerSetupCursor } from "./setup-cursor.mjs";
import { registerSetupRoo } from "./setup-roo.mjs";
import { registerConnect } from "./connect.mjs";
import { registerTokens } from "./tokens.mjs";
import { registerConfigure } from "./configure.mjs";
@@ -138,6 +139,7 @@ export function registerCommands(program) {
registerSetupKilo(program);
registerSetupContinue(program);
registerSetupCursor(program);
registerSetupRoo(program);
registerConnect(program);
registerTokens(program);
registerConfigure(program);

View File

@@ -0,0 +1,172 @@
/**
* omniroute setup-roo — configure Roo Code (RooVeterinaryInc.roo-cline) for OmniRoute.
*
* Roo is a VS Code extension (Cline fork). Its live settings live in opaque VS
* Code globalStorage, but Roo supports **Settings Import** + an
* `roo-cline.autoImportSettingsPath` (VS Code settings.json) that loads a JSON on
* startup. So this writes a best-effort import file + wires autoImport (when a VS
* Code settings.json exists) + prints the UI steps as the guaranteed path.
*
* OpenAI-compatible: baseUrl WITH /v1 (Roo appends /chat/completions). The model
* must support native OpenAI tool-calling (OmniRoute does).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve baseUrl (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveRooTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: ensureV1(root), apiKey };
}
/** Build a Roo Settings-Import document (provider profile, openai-compatible). */
export function buildRooImport({ baseUrl, apiKey, model }) {
return {
providerProfiles: {
currentApiConfigName: "OmniRoute",
apiConfigs: {
OmniRoute: {
apiProvider: "openai",
openAiBaseUrl: baseUrl,
openAiApiKey: apiKey || "sk_omniroute",
openAiModelId: model,
openAiCustomModelInfo: { supportsImages: false, supportsPromptCache: false },
},
},
},
};
}
/** Add the autoImport pointer to a VS Code settings.json object. */
export function buildRooVscodeAutoImport(existing, importPath) {
return { ...(existing || {}), "roo-cline.autoImportSettingsPath": importPath };
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(baseUrl, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupRooCommand(opts = {}) {
const { baseUrl, apiKey } = resolveRooTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const importPath = opts.importPath ?? opts["import-path"] ?? join(os.homedir(), ".omniroute", "roo-settings.json");
const vscodePath =
opts.vscodeSettings ?? opts["vscode-settings"] ?? join(os.homedir(), ".config", "Code", "User", "settings.json");
printHeading("OmniRoute → Roo Code (OpenAI-compatible)");
printInfo(`Server: ${baseUrl}`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(baseUrl, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const { createPrompt } = await import("../io.mjs");
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Roo");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id> (Roo has no model auto-discovery).");
return 2;
}
const importDoc = buildRooImport({ baseUrl, apiKey, model });
const vscodeExists = existsSync(vscodePath);
if (dryRun) {
console.log(`\n── [dry-run] ${importPath} ──`);
console.log(JSON.stringify({ ...importDoc, providerProfiles: { ...importDoc.providerProfiles, apiConfigs: { OmniRoute: { ...importDoc.providerProfiles.apiConfigs.OmniRoute, openAiApiKey: apiKey ? "set" : "sk_omniroute" } } } }, null, 2));
console.log(`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would set roo-cline.autoImportSettingsPath)" : "(skipped — file absent)"}`);
} else {
mkdirSync(join(importPath, ".."), { recursive: true });
writeFileSync(importPath, JSON.stringify(importDoc, null, 2) + "\n", "utf8");
printSuccess(`Wrote ${importPath}`);
if (vscodeExists) {
const merged = buildRooVscodeAutoImport(readJson(vscodePath), importPath);
writeFileSync(vscodePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
printSuccess(`Set roo-cline.autoImportSettingsPath in ${vscodePath}`);
}
}
printInfo("\nIn the Roo Code panel: Settings → Providers → OpenAI Compatible (guaranteed path):");
printInfo(` Base URL: ${baseUrl} (Roo expects /v1)`);
printInfo(` API Key: <your OMNIROUTE_API_KEY>`);
printInfo(` Model: ${model}`);
printInfo(`Or use Roo: “Import Settings” → select ${importPath}`);
return 0;
}
export function registerSetupRoo(program) {
program
.command("setup-roo")
.description(
"Configure Roo Code for OmniRoute: write a Roo import JSON + autoImport pointer + print UI steps"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Roo (required unless picked interactively)")
.option("--import-path <path>", "Roo import JSON path (default: ~/.omniroute/roo-settings.json)")
.option("--vscode-settings <path>", "VS Code settings.json (default: ~/.config/Code/User/settings.json)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupRooCommand(opts);
if (code !== 0) process.exit(code);
});
}

View File

@@ -151,6 +151,7 @@ context, or `--remote <url> --api-key <key>`):
| Kilo Code | `omniroute setup-kilo` | `~/.local/share/kilo/auth.json` (CLI) + VS Code `kilocode.*` settings — OpenAI-compatible, Base URL **with** `/v1` |
| Continue | `omniroute setup-continue` | `~/.continue/config.yaml` (VS Code/JetBrains + `cn` CLI) — `provider: openai`, `apiBase` **with** `/v1`, key via `${{ secrets.OMNIROUTE_API_KEY }}` |
| Cursor | `omniroute setup-cursor` | prints the in-app steps (Settings → Models → Override OpenAI Base URL **with** `/v1` + key + model). Cursor config is opaque SQLite — chat panel only |
| Roo Code | `omniroute setup-roo` | writes a Roo import JSON (`~/.omniroute/roo-settings.json`) + sets `roo-cline.autoImportSettingsPath` + prints UI steps (OpenAI-compatible, Base URL **with** `/v1`) |
```bash
# OpenCode (openai-compatible provider, all catalog models, remote VPS)

View File

@@ -0,0 +1,26 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveRooTarget, buildRooImport, buildRooVscodeAutoImport } from "../../../bin/cli/commands/setup-roo.mjs";
test("resolveRooTarget ensures /v1 on the base URL", () => {
assert.equal(resolveRooTarget({ remote: "http://vps:20128" }).baseUrl, "http://vps:20128/v1");
});
test("resolveRooTarget: explicit --api-key wins", () => {
assert.equal(resolveRooTarget({ remote: "http://x:20128", apiKey: "sk-x" }).apiKey, "sk-x");
});
test("buildRooImport produces an openai-compatible provider profile (baseUrl /v1, model)", () => {
const d = buildRooImport({ baseUrl: "http://vps:20128/v1", apiKey: "k", model: "glm/glm-5.2" });
const cfg = d.providerProfiles.apiConfigs.OmniRoute;
assert.equal(cfg.apiProvider, "openai");
assert.equal(cfg.openAiBaseUrl, "http://vps:20128/v1");
assert.equal(cfg.openAiModelId, "glm/glm-5.2");
assert.equal(d.providerProfiles.currentApiConfigName, "OmniRoute");
});
test("buildRooImport falls back to a placeholder key", () => {
assert.equal(buildRooImport({ baseUrl: "http://x/v1", apiKey: "", model: "m" }).providerProfiles.apiConfigs.OmniRoute.openAiApiKey, "sk_omniroute");
});
test("buildRooVscodeAutoImport sets the pointer, preserving other settings", () => {
const s = buildRooVscodeAutoImport({ "editor.tabSize": 2 }, "/home/u/.omniroute/roo-settings.json");
assert.equal(s["editor.tabSize"], 2);
assert.equal(s["roo-cline.autoImportSettingsPath"], "/home/u/.omniroute/roo-settings.json");
});