mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Compare commits
8 commits
main
...
cli-v0.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0574fe978f | ||
|
|
dc7579f63d | ||
|
|
e41438faaf | ||
|
|
d14e7e200c | ||
|
|
1f08401523 | ||
|
|
80de137735 | ||
|
|
8e65f76ffb | ||
|
|
980681eccb |
14 changed files with 512 additions and 141 deletions
|
|
@ -5,6 +5,24 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.0.47] - 2026-01-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Workspace flag**: New `-w, --workspace <path>` option to specify a custom workspace directory instead of using the current working directory
|
||||||
|
- **Oneshot mode**: New `--oneshot` flag to exit upon task completion, useful for scripting and automation (can also be saved in settings via [`CliSettings.oneshot`](src/types/types.ts))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Skip onboarding flow when a provider is explicitly specified via `--provider` flag or saved in settings
|
||||||
|
- Unified permission flags: Combined `-y`, `--yes`, and `--dangerously-skip-permissions` into a single option for Claude Code-like CLI compatibility
|
||||||
|
- Improved Roo Code Router authentication flow and error messaging
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Removed unnecessary timeout that could cause issues with long-running tasks
|
||||||
|
- Fixed authentication token validation for Roo Code Router provider
|
||||||
|
|
||||||
## [0.0.45] - 2026-01-08
|
## [0.0.45] - 2026-01-08
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@roo-code/cli",
|
"name": "@roo-code/cli",
|
||||||
"version": "0.0.45",
|
"version": "0.0.47",
|
||||||
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"build:extension": "pnpm --filter roo-cline bundle",
|
"build:extension": "pnpm --filter roo-cline bundle",
|
||||||
"build:all": "pnpm --filter roo-cline bundle && tsup",
|
"build:all": "pnpm --filter roo-cline bundle && tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"start": "ROO_SDK_BASE_URL=http://localhost:3001 ROO_AUTH_BASE_URL=http://localhost:3000 node dist/index.js",
|
"start": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy node dist/index.js",
|
||||||
"start:production": "node dist/index.js",
|
"start:production": "node dist/index.js",
|
||||||
"release": "scripts/release.sh",
|
"release": "scripts/release.sh",
|
||||||
"clean": "rimraf dist .turbo"
|
"clean": "rimraf dist .turbo"
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ create_tarball() {
|
||||||
'commander': pkg.dependencies.commander,
|
'commander': pkg.dependencies.commander,
|
||||||
'fuzzysort': pkg.dependencies.fuzzysort,
|
'fuzzysort': pkg.dependencies.fuzzysort,
|
||||||
'ink': pkg.dependencies.ink,
|
'ink': pkg.dependencies.ink,
|
||||||
|
'p-wait-for': pkg.dependencies['p-wait-for'],
|
||||||
'react': pkg.dependencies.react,
|
'react': pkg.dependencies.react,
|
||||||
'superjson': pkg.dependencies.superjson,
|
'superjson': pkg.dependencies.superjson,
|
||||||
'zustand': pkg.dependencies.zustand
|
'zustand': pkg.dependencies.zustand
|
||||||
|
|
@ -421,7 +422,7 @@ verify_local_install() {
|
||||||
|
|
||||||
# Run the CLI with a simple prompt
|
# Run the CLI with a simple prompt
|
||||||
# Use timeout to prevent hanging if something goes wrong
|
# Use timeout to prevent hanging if something goes wrong
|
||||||
if timeout 60 "$VERIFY_BIN_DIR/roo" --yes --exit-on-complete --prompt "1+1=?" "$VERIFY_WORKSPACE" > "$VERIFY_DIR/test-output.log" 2>&1; then
|
if timeout 60 "$VERIFY_BIN_DIR/roo" --yes --oneshot --print "1+1=?" > "$VERIFY_DIR/test-output.log" 2>&1; then
|
||||||
info "End-to-end test passed"
|
info "End-to-end test passed"
|
||||||
else
|
else
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
|
|
|
||||||
|
|
@ -437,9 +437,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
||||||
this.sendToExtension({ type: "newTask", text: prompt })
|
this.sendToExtension({ type: "newTask", text: prompt })
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timeoutId: NodeJS.Timeout | null = null
|
|
||||||
const timeoutMs: number = 110_000
|
|
||||||
|
|
||||||
const completeHandler = () => {
|
const completeHandler = () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
resolve()
|
resolve()
|
||||||
|
|
@ -451,23 +448,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
timeoutId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.off("taskCompleted", completeHandler)
|
this.client.off("taskCompleted", completeHandler)
|
||||||
this.client.off("error", errorHandler)
|
this.client.off("error", errorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout to prevent indefinite hanging.
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
cleanup()
|
|
||||||
reject(
|
|
||||||
new Error(`Task completion timeout after ${timeoutMs}ms - no completion or error event received`),
|
|
||||||
)
|
|
||||||
}, timeoutMs)
|
|
||||||
|
|
||||||
this.client.once("taskCompleted", completeHandler)
|
this.client.once("taskCompleted", completeHandler)
|
||||||
this.client.once("error", errorHandler)
|
this.client.once("error", errorHandler)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,15 @@ export interface LoginOptions {
|
||||||
verbose?: boolean
|
verbose?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResult {
|
export type LoginResult =
|
||||||
success: boolean
|
| {
|
||||||
error?: string
|
success: true
|
||||||
userId?: string
|
token: string
|
||||||
orgId?: string | null
|
}
|
||||||
}
|
| {
|
||||||
|
success: false
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
const LOCALHOST = "127.0.0.1"
|
const LOCALHOST = "127.0.0.1"
|
||||||
|
|
||||||
|
|
@ -29,49 +32,57 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
||||||
console.log(`[Auth] Starting local callback server on port ${port}`)
|
console.log(`[Auth] Starting local callback server on port ${port}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": AUTH_BASE_URL,
|
||||||
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
}
|
||||||
|
|
||||||
// Create promise that will be resolved when we receive the callback.
|
// Create promise that will be resolved when we receive the callback.
|
||||||
const tokenPromise = new Promise<{ token: string; state: string }>((resolve, reject) => {
|
const tokenPromise = new Promise<{ token: string; state: string }>((resolve, reject) => {
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
const url = new URL(req.url!, host)
|
const url = new URL(req.url!, host)
|
||||||
|
|
||||||
if (url.pathname === "/callback") {
|
// Handle CORS preflight request.
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.writeHead(204, corsHeaders)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/callback" && req.method === "POST") {
|
||||||
const receivedState = url.searchParams.get("state")
|
const receivedState = url.searchParams.get("state")
|
||||||
const token = url.searchParams.get("token")
|
const token = url.searchParams.get("token")
|
||||||
const error = url.searchParams.get("error")
|
const error = url.searchParams.get("error")
|
||||||
|
|
||||||
|
const sendJsonResponse = (status: number, body: object) => {
|
||||||
|
res.writeHead(status, {
|
||||||
|
...corsHeaders,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
res.end(JSON.stringify(body))
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=error-in-callback`)
|
sendJsonResponse(400, { success: false, error })
|
||||||
errorUrl.searchParams.set("message", error)
|
|
||||||
res.writeHead(302, { Location: errorUrl.toString() })
|
|
||||||
res.end()
|
|
||||||
// Wait for response to be fully sent before closing server and rejecting.
|
|
||||||
// The 'close' event fires when the underlying connection is terminated,
|
|
||||||
// ensuring the browser has received the redirect before we shut down.
|
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
server.close()
|
server.close()
|
||||||
reject(new Error(error))
|
reject(new Error(error))
|
||||||
})
|
})
|
||||||
} else if (!token) {
|
} else if (!token) {
|
||||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=missing-token`)
|
sendJsonResponse(400, { success: false, error: "Missing token in callback" })
|
||||||
errorUrl.searchParams.set("message", "Missing token in callback")
|
|
||||||
res.writeHead(302, { Location: errorUrl.toString() })
|
|
||||||
res.end()
|
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
server.close()
|
server.close()
|
||||||
reject(new Error("Missing token in callback"))
|
reject(new Error("Missing token in callback"))
|
||||||
})
|
})
|
||||||
} else if (receivedState !== state) {
|
} else if (receivedState !== state) {
|
||||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=invalid-state-parameter`)
|
sendJsonResponse(400, { success: false, error: "Invalid state parameter" })
|
||||||
errorUrl.searchParams.set("message", "Invalid state parameter (possible CSRF attack)")
|
|
||||||
res.writeHead(302, { Location: errorUrl.toString() })
|
|
||||||
res.end()
|
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
server.close()
|
server.close()
|
||||||
reject(new Error("Invalid state parameter"))
|
reject(new Error("Invalid state parameter"))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(302, { Location: `${AUTH_BASE_URL}/cli/sign-in?success=true` })
|
sendJsonResponse(200, { success: true })
|
||||||
res.end()
|
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
server.close()
|
server.close()
|
||||||
resolve({ token, state: receivedState })
|
resolve({ token, state: receivedState })
|
||||||
|
|
@ -90,12 +101,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
||||||
reject(new Error("Authentication timed out"))
|
reject(new Error("Authentication timed out"))
|
||||||
}, timeout)
|
}, timeout)
|
||||||
|
|
||||||
server.on("listening", () => {
|
|
||||||
console.log(`[Auth] Callback server listening on port ${port}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
server.on("close", () => {
|
server.on("close", () => {
|
||||||
console.log("[Auth] Callback server closed")
|
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -121,7 +127,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
||||||
const { token } = await tokenPromise
|
const { token } = await tokenPromise
|
||||||
await saveToken(token)
|
await saveToken(token)
|
||||||
console.log("✓ Successfully authenticated!")
|
console.log("✓ Successfully authenticated!")
|
||||||
return { success: true }
|
return { success: true, token }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
console.error(`✗ Authentication failed: ${message}`)
|
console.error(`✗ Authentication failed: ${message}`)
|
||||||
|
|
|
||||||
93
apps/cli/src/commands/cli/__tests__/run.test.ts
Normal file
93
apps/cli/src/commands/cli/__tests__/run.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
|
||||||
|
describe("run command --prompt-file option", () => {
|
||||||
|
let tempDir: string
|
||||||
|
let promptFilePath: string
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-test-"))
|
||||||
|
promptFilePath = path.join(tempDir, "prompt.md")
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should read prompt from file when --prompt-file is provided", () => {
|
||||||
|
const promptContent = `This is a test prompt with special characters:
|
||||||
|
- Quotes: "hello" and 'world'
|
||||||
|
- Backticks: \`code\`
|
||||||
|
- Newlines and tabs
|
||||||
|
- Unicode: 你好 🎉`
|
||||||
|
|
||||||
|
fs.writeFileSync(promptFilePath, promptContent)
|
||||||
|
|
||||||
|
// Verify the file was written correctly
|
||||||
|
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||||
|
expect(readContent).toBe(promptContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle multi-line prompts correctly", () => {
|
||||||
|
const multiLinePrompt = `Line 1
|
||||||
|
Line 2
|
||||||
|
Line 3
|
||||||
|
|
||||||
|
Empty line above
|
||||||
|
\tTabbed line
|
||||||
|
Indented line`
|
||||||
|
|
||||||
|
fs.writeFileSync(promptFilePath, multiLinePrompt)
|
||||||
|
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||||
|
|
||||||
|
expect(readContent).toBe(multiLinePrompt)
|
||||||
|
expect(readContent.split("\n")).toHaveLength(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle very long prompts that would exceed ARG_MAX", () => {
|
||||||
|
// ARG_MAX is typically 128KB-2MB, so let's test with a 500KB prompt
|
||||||
|
const longPrompt = "x".repeat(500 * 1024)
|
||||||
|
|
||||||
|
fs.writeFileSync(promptFilePath, longPrompt)
|
||||||
|
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||||
|
|
||||||
|
expect(readContent.length).toBe(500 * 1024)
|
||||||
|
expect(readContent).toBe(longPrompt)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should preserve shell-sensitive characters", () => {
|
||||||
|
const shellSensitivePrompt = `
|
||||||
|
$HOME
|
||||||
|
$(echo dangerous)
|
||||||
|
\`rm -rf /\`
|
||||||
|
"quoted string"
|
||||||
|
'single quoted'
|
||||||
|
$((1+1))
|
||||||
|
&&
|
||||||
|
||
|
||||||
|
;
|
||||||
|
> /dev/null
|
||||||
|
< input.txt
|
||||||
|
| grep something
|
||||||
|
*
|
||||||
|
?
|
||||||
|
[abc]
|
||||||
|
{a,b}
|
||||||
|
~
|
||||||
|
!
|
||||||
|
#comment
|
||||||
|
%s
|
||||||
|
\n\t\r
|
||||||
|
`
|
||||||
|
|
||||||
|
fs.writeFileSync(promptFilePath, shellSensitivePrompt)
|
||||||
|
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||||
|
|
||||||
|
// All shell-sensitive characters should be preserved exactly
|
||||||
|
expect(readContent).toBe(shellSensitivePrompt)
|
||||||
|
expect(readContent).toContain("$HOME")
|
||||||
|
expect(readContent).toContain("$(echo dangerous)")
|
||||||
|
expect(readContent).toContain("`rm -rf /`")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -28,7 +28,7 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js"
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
export async function run(workspaceArg: string, flagOptions: FlagOptions) {
|
export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
|
||||||
setLogger({
|
setLogger({
|
||||||
info: () => {},
|
info: () => {},
|
||||||
warn: () => {},
|
warn: () => {},
|
||||||
|
|
@ -36,34 +36,60 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
|
||||||
debug: () => {},
|
debug: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let prompt = promptArg
|
||||||
|
|
||||||
|
if (flagOptions.promptFile) {
|
||||||
|
if (!fs.existsSync(flagOptions.promptFile)) {
|
||||||
|
console.error(`[CLI] Error: Prompt file does not exist: ${flagOptions.promptFile}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt = fs.readFileSync(flagOptions.promptFile, "utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
|
|
||||||
|
let rooToken = await loadToken()
|
||||||
|
const settings = await loadSettings()
|
||||||
|
|
||||||
const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
|
const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
|
||||||
const isTuiEnabled = flagOptions.tui && isTuiSupported
|
const isTuiEnabled = !flagOptions.print && isTuiSupported
|
||||||
const rooToken = await loadToken()
|
const isOnboardingEnabled = isTuiEnabled && !rooToken && !flagOptions.provider && !settings.provider
|
||||||
|
|
||||||
|
// Determine effective values: CLI flags > settings file > DEFAULT_FLAGS.
|
||||||
|
const effectiveMode = flagOptions.mode || settings.mode || DEFAULT_FLAGS.mode
|
||||||
|
const effectiveModel = flagOptions.model || settings.model || DEFAULT_FLAGS.model
|
||||||
|
const effectiveReasoningEffort =
|
||||||
|
flagOptions.reasoningEffort || settings.reasoningEffort || DEFAULT_FLAGS.reasoningEffort
|
||||||
|
const effectiveProvider = flagOptions.provider ?? settings.provider ?? (rooToken ? "roo" : "openrouter")
|
||||||
|
const effectiveWorkspacePath = flagOptions.workspace ? path.resolve(flagOptions.workspace) : process.cwd()
|
||||||
|
const effectiveDangerouslySkipPermissions =
|
||||||
|
flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false
|
||||||
|
const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false
|
||||||
|
|
||||||
const extensionHostOptions: ExtensionHostOptions = {
|
const extensionHostOptions: ExtensionHostOptions = {
|
||||||
mode: flagOptions.mode || DEFAULT_FLAGS.mode,
|
mode: effectiveMode,
|
||||||
reasoningEffort: flagOptions.reasoningEffort === "unspecified" ? undefined : flagOptions.reasoningEffort,
|
reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort,
|
||||||
user: null,
|
user: null,
|
||||||
provider: flagOptions.provider ?? (rooToken ? "roo" : "openrouter"),
|
provider: effectiveProvider,
|
||||||
model: flagOptions.model || DEFAULT_FLAGS.model,
|
model: effectiveModel,
|
||||||
workspacePath: path.resolve(workspaceArg),
|
workspacePath: effectiveWorkspacePath,
|
||||||
extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
|
extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
|
||||||
nonInteractive: flagOptions.yes,
|
nonInteractive: effectiveDangerouslySkipPermissions,
|
||||||
ephemeral: flagOptions.ephemeral,
|
ephemeral: flagOptions.ephemeral,
|
||||||
debug: flagOptions.debug,
|
debug: flagOptions.debug,
|
||||||
exitOnComplete: flagOptions.exitOnComplete,
|
exitOnComplete: effectiveExitOnComplete,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roo Code Cloud Authentication
|
// Roo Code Cloud Authentication
|
||||||
|
|
||||||
if (isTuiEnabled) {
|
if (isOnboardingEnabled) {
|
||||||
let { onboardingProviderChoice } = await loadSettings()
|
let { onboardingProviderChoice } = settings
|
||||||
|
|
||||||
if (!onboardingProviderChoice) {
|
if (!onboardingProviderChoice) {
|
||||||
const result = await runOnboarding()
|
const { choice, token } = await runOnboarding()
|
||||||
onboardingProviderChoice = result.choice
|
onboardingProviderChoice = choice
|
||||||
|
rooToken = token ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onboardingProviderChoice === OnboardingProviderChoice.Roo) {
|
if (onboardingProviderChoice === OnboardingProviderChoice.Roo) {
|
||||||
|
|
@ -139,15 +165,15 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTuiEnabled) {
|
if (!isTuiEnabled) {
|
||||||
if (!flagOptions.prompt) {
|
if (!prompt) {
|
||||||
console.error("[CLI] Error: prompt is required in plain text mode")
|
console.error("[CLI] Error: prompt is required in print mode")
|
||||||
console.error("[CLI] Usage: roo [workspace] -P <prompt> [options]")
|
console.error("[CLI] Usage: roo <prompt> --print [options]")
|
||||||
console.error("[CLI] Use TUI mode (without --no-tui) for interactive input")
|
console.error("[CLI] Run without -p for interactive mode")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flagOptions.tui) {
|
if (!flagOptions.print) {
|
||||||
console.warn("[CLI] TUI disabled (no TTY support), falling back to plain text mode")
|
console.warn("[CLI] TUI disabled (no TTY support), falling back to print mode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +187,7 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
|
||||||
render(
|
render(
|
||||||
createElement(App, {
|
createElement(App, {
|
||||||
...extensionHostOptions,
|
...extensionHostOptions,
|
||||||
initialPrompt: flagOptions.prompt,
|
initialPrompt: prompt,
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts),
|
createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts),
|
||||||
}),
|
}),
|
||||||
|
|
@ -200,12 +226,9 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await host.activate()
|
await host.activate()
|
||||||
await host.runTask(flagOptions.prompt!)
|
await host.runTask(prompt!)
|
||||||
await host.dispose()
|
await host.dispose()
|
||||||
|
process.exit(0)
|
||||||
if (!flagOptions.waitOnComplete) {
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,31 +6,30 @@ import { run, login, logout, status } from "@/commands/index.js"
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
|
|
||||||
program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version(VERSION)
|
program
|
||||||
|
.name("roo")
|
||||||
|
.description("Roo Code CLI - starts an interactive session by default, use -p/--print for non-interactive output")
|
||||||
|
.version(VERSION)
|
||||||
|
|
||||||
program
|
program
|
||||||
.argument("[workspace]", "Workspace path to operate in", process.cwd())
|
.argument("[prompt]", "Your prompt")
|
||||||
.option("-P, --prompt <prompt>", "The prompt/task to execute (optional in TUI mode)")
|
.option("--prompt-file <path>", "Read prompt from a file instead of command line argument")
|
||||||
|
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
|
||||||
|
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
|
||||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
||||||
.option("-y, --yes", "Auto-approve all prompts (non-interactive mode)", false)
|
.option("-y, --yes, --dangerously-skip-permissions", "Auto-approve all prompts (use with caution)", false)
|
||||||
.option("-k, --api-key <key>", "API key for the LLM provider")
|
.option("-k, --api-key <key>", "API key for the LLM provider")
|
||||||
.option("-p, --provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
|
.option("--provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
|
||||||
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)
|
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)
|
||||||
.option("-M, --mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode)
|
.option("--mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode)
|
||||||
.option(
|
.option(
|
||||||
"-r, --reasoning-effort <effort>",
|
"-r, --reasoning-effort <effort>",
|
||||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||||
DEFAULT_FLAGS.reasoningEffort,
|
DEFAULT_FLAGS.reasoningEffort,
|
||||||
)
|
)
|
||||||
.option("-x, --exit-on-complete", "Exit the process when the task completes (applies to TUI mode only)", false)
|
|
||||||
.option(
|
|
||||||
"-w, --wait-on-complete",
|
|
||||||
"Keep the process running when the task completes (applies to plain text mode only)",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
|
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
|
||||||
.option("--no-tui", "Disable TUI, use plain text output")
|
.option("--oneshot", "Exit upon task completion", false)
|
||||||
.action(run)
|
.action(run)
|
||||||
|
|
||||||
const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud")
|
const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud")
|
||||||
|
|
|
||||||
236
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
236
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
// Use vi.hoisted to make the test directory available to the mock
|
||||||
|
// This must return the path synchronously since settings path is computed at import time
|
||||||
|
const { getTestConfigDir } = vi.hoisted(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const os = require("os")
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const path = require("path")
|
||||||
|
const testRunId = Date.now().toString()
|
||||||
|
const testConfigDir = path.join(os.tmpdir(), `roo-cli-settings-test-${testRunId}`)
|
||||||
|
return { getTestConfigDir: () => testConfigDir }
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock("../config-dir.js", () => ({
|
||||||
|
getConfigDir: getTestConfigDir,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Import after mocking
|
||||||
|
import { loadSettings, saveSettings, resetOnboarding, getSettingsPath } from "../settings.js"
|
||||||
|
import { OnboardingProviderChoice } from "@/types/index.js"
|
||||||
|
|
||||||
|
// Re-derive the test config dir for use in tests (must match the hoisted one)
|
||||||
|
const actualTestConfigDir = getTestConfigDir()
|
||||||
|
|
||||||
|
describe("Settings Storage", () => {
|
||||||
|
const expectedSettingsFile = path.join(actualTestConfigDir, "cli-settings.json")
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clear test directory before each test
|
||||||
|
await fs.rm(actualTestConfigDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up test directory
|
||||||
|
await fs.rm(actualTestConfigDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getSettingsPath", () => {
|
||||||
|
it("should return the correct settings file path", () => {
|
||||||
|
expect(getSettingsPath()).toBe(expectedSettingsFile)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("loadSettings", () => {
|
||||||
|
it("should return empty object if no settings file exists", async () => {
|
||||||
|
const settings = await loadSettings()
|
||||||
|
expect(settings).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should load saved settings", async () => {
|
||||||
|
const settingsData = {
|
||||||
|
onboardingProviderChoice: OnboardingProviderChoice.Roo,
|
||||||
|
mode: "architect",
|
||||||
|
provider: "anthropic" as const,
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
reasoningEffort: "high" as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(actualTestConfigDir, { recursive: true })
|
||||||
|
await fs.writeFile(expectedSettingsFile, JSON.stringify(settingsData), "utf-8")
|
||||||
|
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
expect(loaded).toEqual(settingsData)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should load settings with only some fields set", async () => {
|
||||||
|
const settingsData = {
|
||||||
|
mode: "code",
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(actualTestConfigDir, { recursive: true })
|
||||||
|
await fs.writeFile(expectedSettingsFile, JSON.stringify(settingsData), "utf-8")
|
||||||
|
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
expect(loaded).toEqual(settingsData)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("saveSettings", () => {
|
||||||
|
it("should save settings to disk", async () => {
|
||||||
|
await saveSettings({ mode: "debug" })
|
||||||
|
|
||||||
|
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||||
|
const settings = JSON.parse(savedData)
|
||||||
|
|
||||||
|
expect(settings.mode).toBe("debug")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should merge settings with existing ones", async () => {
|
||||||
|
await saveSettings({ mode: "code" })
|
||||||
|
await saveSettings({ provider: "openrouter" as const })
|
||||||
|
|
||||||
|
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||||
|
const settings = JSON.parse(savedData)
|
||||||
|
|
||||||
|
expect(settings.mode).toBe("code")
|
||||||
|
expect(settings.provider).toBe("openrouter")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should save all default settings fields", async () => {
|
||||||
|
await saveSettings({
|
||||||
|
mode: "architect",
|
||||||
|
provider: "anthropic" as const,
|
||||||
|
model: "claude-opus-4.5",
|
||||||
|
reasoningEffort: "medium" as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||||
|
const settings = JSON.parse(savedData)
|
||||||
|
|
||||||
|
expect(settings.mode).toBe("architect")
|
||||||
|
expect(settings.provider).toBe("anthropic")
|
||||||
|
expect(settings.model).toBe("claude-opus-4.5")
|
||||||
|
expect(settings.reasoningEffort).toBe("medium")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create config directory if it doesn't exist", async () => {
|
||||||
|
await saveSettings({ mode: "ask" })
|
||||||
|
|
||||||
|
const dirStats = await fs.stat(actualTestConfigDir)
|
||||||
|
expect(dirStats.isDirectory()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unix file permissions don't apply on Windows - skip this test
|
||||||
|
it.skipIf(process.platform === "win32")("should set restrictive file permissions", async () => {
|
||||||
|
await saveSettings({ mode: "code" })
|
||||||
|
|
||||||
|
const stats = await fs.stat(expectedSettingsFile)
|
||||||
|
// Check that only owner has read/write (mode 0o600)
|
||||||
|
const mode = stats.mode & 0o777
|
||||||
|
expect(mode).toBe(0o600)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resetOnboarding", () => {
|
||||||
|
it("should reset onboarding provider choice", async () => {
|
||||||
|
await saveSettings({ onboardingProviderChoice: OnboardingProviderChoice.Roo })
|
||||||
|
|
||||||
|
await resetOnboarding()
|
||||||
|
|
||||||
|
const settings = await loadSettings()
|
||||||
|
expect(settings.onboardingProviderChoice).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should preserve other settings when resetting onboarding", async () => {
|
||||||
|
await saveSettings({
|
||||||
|
onboardingProviderChoice: OnboardingProviderChoice.Byok,
|
||||||
|
mode: "architect",
|
||||||
|
provider: "gemini" as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
await resetOnboarding()
|
||||||
|
|
||||||
|
const settings = await loadSettings()
|
||||||
|
expect(settings.onboardingProviderChoice).toBeUndefined()
|
||||||
|
expect(settings.mode).toBe("architect")
|
||||||
|
expect(settings.provider).toBe("gemini")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("default settings priority", () => {
|
||||||
|
it("should support all configurable default settings", async () => {
|
||||||
|
// Test that all the settings that can be used as defaults are properly saved and loaded
|
||||||
|
const defaultSettings = {
|
||||||
|
mode: "debug",
|
||||||
|
provider: "openai-native" as const,
|
||||||
|
model: "gpt-4o",
|
||||||
|
reasoningEffort: "low" as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveSettings(defaultSettings)
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
|
||||||
|
expect(loaded.mode).toBe("debug")
|
||||||
|
expect(loaded.provider).toBe("openai-native")
|
||||||
|
expect(loaded.model).toBe("gpt-4o")
|
||||||
|
expect(loaded.reasoningEffort).toBe("low")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should support dangerouslySkipPermissions setting", async () => {
|
||||||
|
await saveSettings({ dangerouslySkipPermissions: true })
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
|
||||||
|
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should support all settings together including dangerouslySkipPermissions", async () => {
|
||||||
|
const allSettings = {
|
||||||
|
mode: "architect",
|
||||||
|
provider: "anthropic" as const,
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
reasoningEffort: "high" as const,
|
||||||
|
dangerouslySkipPermissions: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveSettings(allSettings)
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
|
||||||
|
expect(loaded.mode).toBe("architect")
|
||||||
|
expect(loaded.provider).toBe("anthropic")
|
||||||
|
expect(loaded.model).toBe("claude-sonnet-4-20250514")
|
||||||
|
expect(loaded.reasoningEffort).toBe("high")
|
||||||
|
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should support oneshot setting", async () => {
|
||||||
|
await saveSettings({ oneshot: true })
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
|
||||||
|
expect(loaded.oneshot).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should support all settings together including oneshot", async () => {
|
||||||
|
const allSettings = {
|
||||||
|
mode: "architect",
|
||||||
|
provider: "anthropic" as const,
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
reasoningEffort: "high" as const,
|
||||||
|
dangerouslySkipPermissions: true,
|
||||||
|
oneshot: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveSettings(allSettings)
|
||||||
|
const loaded = await loadSettings()
|
||||||
|
|
||||||
|
expect(loaded.mode).toBe("architect")
|
||||||
|
expect(loaded.provider).toBe("anthropic")
|
||||||
|
expect(loaded.model).toBe("claude-sonnet-4-20250514")
|
||||||
|
expect(loaded.reasoningEffort).toBe("high")
|
||||||
|
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||||
|
expect(loaded.oneshot).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -17,9 +17,14 @@ export async function runOnboarding(): Promise<OnboardingResult> {
|
||||||
console.log("")
|
console.log("")
|
||||||
|
|
||||||
if (choice === OnboardingProviderChoice.Roo) {
|
if (choice === OnboardingProviderChoice.Roo) {
|
||||||
const { success: authenticated } = await login()
|
const result = await login()
|
||||||
await saveSettings({ onboardingProviderChoice: choice })
|
await saveSettings({ onboardingProviderChoice: choice })
|
||||||
resolve({ choice: OnboardingProviderChoice.Roo, authenticated, skipped: false })
|
|
||||||
|
resolve({
|
||||||
|
choice: OnboardingProviderChoice.Roo,
|
||||||
|
token: result.success ? result.token : undefined,
|
||||||
|
skipped: false,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log("Using your own API key.")
|
console.log("Using your own API key.")
|
||||||
console.log("Set your API key via --api-key or environment variable.")
|
console.log("Set your API key via --api-key or environment variable.")
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,20 @@ export function isSupportedProvider(provider: string): provider is SupportedProv
|
||||||
export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled"
|
export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled"
|
||||||
|
|
||||||
export type FlagOptions = {
|
export type FlagOptions = {
|
||||||
prompt?: string
|
promptFile?: string
|
||||||
|
workspace?: string
|
||||||
|
print: boolean
|
||||||
extension?: string
|
extension?: string
|
||||||
debug: boolean
|
debug: boolean
|
||||||
yes: boolean
|
yes: boolean
|
||||||
|
dangerouslySkipPermissions: boolean
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
provider?: SupportedProvider
|
provider?: SupportedProvider
|
||||||
model?: string
|
model?: string
|
||||||
mode?: string
|
mode?: string
|
||||||
reasoningEffort?: ReasoningEffortFlagOptions
|
reasoningEffort?: ReasoningEffortFlagOptions
|
||||||
exitOnComplete: boolean
|
|
||||||
waitOnComplete: boolean
|
|
||||||
ephemeral: boolean
|
ephemeral: boolean
|
||||||
tui: boolean
|
oneshot: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OnboardingProviderChoice {
|
export enum OnboardingProviderChoice {
|
||||||
|
|
@ -40,10 +41,22 @@ export enum OnboardingProviderChoice {
|
||||||
|
|
||||||
export interface OnboardingResult {
|
export interface OnboardingResult {
|
||||||
choice: OnboardingProviderChoice
|
choice: OnboardingProviderChoice
|
||||||
authenticated?: boolean
|
token?: string
|
||||||
skipped: boolean
|
skipped: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliSettings {
|
export interface CliSettings {
|
||||||
onboardingProviderChoice?: OnboardingProviderChoice
|
onboardingProviderChoice?: OnboardingProviderChoice
|
||||||
|
/** Default mode to use (e.g., "code", "architect", "ask", "debug") */
|
||||||
|
mode?: string
|
||||||
|
/** Default provider to use */
|
||||||
|
provider?: SupportedProvider
|
||||||
|
/** Default model to use */
|
||||||
|
model?: string
|
||||||
|
/** Default reasoning effort level */
|
||||||
|
reasoningEffort?: ReasoningEffortFlagOptions
|
||||||
|
/** Auto-approve all prompts (use with caution) */
|
||||||
|
dangerouslySkipPermissions?: boolean
|
||||||
|
/** Exit upon task completion */
|
||||||
|
oneshot?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,23 +68,24 @@ export interface TUIAppProps extends ExtensionHostOptions {
|
||||||
/**
|
/**
|
||||||
* Inner App component that uses the terminal size context
|
* Inner App component that uses the terminal size context
|
||||||
*/
|
*/
|
||||||
function AppInner({
|
function AppInner({ createExtensionHost, ...extensionHostOptions }: TUIAppProps) {
|
||||||
initialPrompt,
|
const {
|
||||||
workspacePath,
|
initialPrompt,
|
||||||
extensionPath,
|
workspacePath,
|
||||||
user,
|
extensionPath,
|
||||||
provider,
|
user,
|
||||||
apiKey,
|
provider,
|
||||||
model,
|
apiKey,
|
||||||
mode,
|
model,
|
||||||
nonInteractive = false,
|
mode,
|
||||||
debug,
|
nonInteractive = false,
|
||||||
exitOnComplete,
|
debug,
|
||||||
reasoningEffort,
|
exitOnComplete,
|
||||||
ephemeral,
|
reasoningEffort,
|
||||||
version,
|
ephemeral,
|
||||||
createExtensionHost,
|
version,
|
||||||
}: TUIAppProps) {
|
} = extensionHostOptions
|
||||||
|
|
||||||
const { exit } = useApp()
|
const { exit } = useApp()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -454,12 +455,7 @@ function AppInner({
|
||||||
{/* Header - fixed size */}
|
{/* Header - fixed size */}
|
||||||
<Box flexShrink={0}>
|
<Box flexShrink={0}>
|
||||||
<Header
|
<Header
|
||||||
cwd={workspacePath}
|
{...extensionHostOptions}
|
||||||
user={user}
|
|
||||||
provider={provider}
|
|
||||||
model={model}
|
|
||||||
mode={currentMode || mode}
|
|
||||||
reasoningEffort={reasoningEffort}
|
|
||||||
version={version}
|
version={version}
|
||||||
tokenUsage={tokenUsage}
|
tokenUsage={tokenUsage}
|
||||||
contextWindow={contextWindow}
|
contextWindow={contextWindow}
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,27 @@ import { Text, Box } from "ink"
|
||||||
import type { TokenUsage } from "@roo-code/types"
|
import type { TokenUsage } from "@roo-code/types"
|
||||||
|
|
||||||
import { ASCII_ROO } from "@/types/constants.js"
|
import { ASCII_ROO } from "@/types/constants.js"
|
||||||
import { User } from "@/lib/sdk/types.js"
|
|
||||||
|
|
||||||
|
import { ExtensionHostOptions } from "@/agent/index.js"
|
||||||
import { useTerminalSize } from "../hooks/TerminalSizeContext.js"
|
import { useTerminalSize } from "../hooks/TerminalSizeContext.js"
|
||||||
import * as theme from "../theme.js"
|
import * as theme from "../theme.js"
|
||||||
|
|
||||||
import MetricsDisplay from "./MetricsDisplay.js"
|
import MetricsDisplay from "./MetricsDisplay.js"
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps extends ExtensionHostOptions {
|
||||||
cwd: string
|
|
||||||
user: User | null
|
|
||||||
provider: string
|
|
||||||
model: string
|
|
||||||
mode: string
|
|
||||||
reasoningEffort?: string
|
|
||||||
version: string
|
version: string
|
||||||
tokenUsage?: TokenUsage | null
|
tokenUsage?: TokenUsage | null
|
||||||
contextWindow?: number
|
contextWindow?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header({
|
function Header({
|
||||||
cwd,
|
workspacePath,
|
||||||
user,
|
user,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
mode,
|
mode,
|
||||||
reasoningEffort,
|
reasoningEffort,
|
||||||
|
nonInteractive,
|
||||||
version,
|
version,
|
||||||
tokenUsage,
|
tokenUsage,
|
||||||
contextWindow,
|
contextWindow,
|
||||||
|
|
@ -53,12 +48,16 @@ function Header({
|
||||||
<Box flexDirection="column" marginLeft={1} marginTop={1}>
|
<Box flexDirection="column" marginLeft={1} marginTop={1}>
|
||||||
{user && <Text color={theme.dimText}>Welcome back, {user.name}</Text>}
|
{user && <Text color={theme.dimText}>Welcome back, {user.name}</Text>}
|
||||||
<Text color={theme.dimText}>
|
<Text color={theme.dimText}>
|
||||||
cwd: {cwd.startsWith(homeDir) ? cwd.replace(homeDir, "~") : cwd}
|
cwd:{" "}
|
||||||
|
{workspacePath.startsWith(homeDir) ? workspacePath.replace(homeDir, "~") : workspacePath}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={theme.dimText}>
|
<Text color={theme.dimText}>
|
||||||
{provider}: {model} [{reasoningEffort}]
|
{provider}: {model} [{reasoningEffort}]
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={theme.dimText}>mode: {mode}</Text>
|
<Text color={theme.dimText}>
|
||||||
|
mode: {mode}
|
||||||
|
{nonInteractive && " (YOLO)"}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as os from "node:os"
|
import * as os from "node:os"
|
||||||
|
|
||||||
|
|
@ -20,7 +19,7 @@ import { mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
||||||
*/
|
*/
|
||||||
export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||||
const { language, exercise } = task
|
const { language, exercise } = task
|
||||||
const prompt = fs.readFileSync(path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`), "utf-8")
|
const promptSourcePath = path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`)
|
||||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||||
const ipcSocketPath = path.resolve(os.tmpdir(), `evals-cli-${run.id}-${task.id}.sock`)
|
const ipcSocketPath = path.resolve(os.tmpdir(), `evals-cli-${run.id}-${task.id}.sock`)
|
||||||
|
|
||||||
|
|
@ -40,32 +39,31 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R
|
||||||
"--filter",
|
"--filter",
|
||||||
"@roo-code/cli",
|
"@roo-code/cli",
|
||||||
"start",
|
"start",
|
||||||
"--yes",
|
"--prompt-file",
|
||||||
"--exit-on-complete",
|
promptSourcePath,
|
||||||
"--reasoning-effort",
|
|
||||||
"disabled",
|
|
||||||
"--workspace",
|
"--workspace",
|
||||||
workspacePath,
|
workspacePath,
|
||||||
|
"--yes",
|
||||||
|
"--reasoning-effort",
|
||||||
|
"disabled",
|
||||||
|
"--oneshot",
|
||||||
]
|
]
|
||||||
|
|
||||||
if (run.settings?.mode) {
|
if (run.settings?.mode) {
|
||||||
cliArgs.push("-M", run.settings.mode)
|
cliArgs.push("--mode", run.settings.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (run.settings?.apiProvider) {
|
if (run.settings?.apiProvider) {
|
||||||
cliArgs.push("-p", run.settings.apiProvider)
|
cliArgs.push("--provider", run.settings.apiProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
|
const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
|
||||||
|
|
||||||
if (modelId) {
|
if (modelId) {
|
||||||
cliArgs.push("-m", modelId)
|
cliArgs.push("--model", modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
cliArgs.push(prompt)
|
|
||||||
|
|
||||||
logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`)
|
logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`)
|
||||||
|
|
||||||
const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() })
|
const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() })
|
||||||
|
|
||||||
// Buffer for accumulating streaming output until we have complete lines.
|
// Buffer for accumulating streaming output until we have complete lines.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue