Просмотр исходного кода

Claude-like cli flags, auth fixes (#10797)

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: Roo Code <[email protected]>
Chris Estreich 5 дней назад
Родитель
Сommit
8fa2c1d598

+ 1 - 1
apps/cli/package.json

@@ -17,7 +17,7 @@
 		"build:extension": "pnpm --filter roo-cline bundle",
 		"build:all": "pnpm --filter roo-cline bundle && tsup",
 		"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",
 		"release": "scripts/release.sh",
 		"clean": "rimraf dist .turbo"

+ 1 - 1
apps/cli/scripts/release.sh

@@ -421,7 +421,7 @@ verify_local_install() {
     
     # Run the CLI with a simple prompt
     # 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 -w "$VERIFY_WORKSPACE" "1+1=?" > "$VERIFY_DIR/test-output.log" 2>&1; then
         info "End-to-end test passed"
     else
         EXIT_CODE=$?

+ 0 - 16
apps/cli/src/agent/extension-host.ts

@@ -437,9 +437,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
 		this.sendToExtension({ type: "newTask", text: prompt })
 
 		return new Promise((resolve, reject) => {
-			let timeoutId: NodeJS.Timeout | null = null
-			const timeoutMs: number = 110_000
-
 			const completeHandler = () => {
 				cleanup()
 				resolve()
@@ -451,23 +448,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
 			}
 
 			const cleanup = () => {
-				if (timeoutId) {
-					clearTimeout(timeoutId)
-					timeoutId = null
-				}
-
 				this.client.off("taskCompleted", completeHandler)
 				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("error", errorHandler)
 		})

+ 36 - 30
apps/cli/src/commands/auth/login.ts

@@ -11,12 +11,15 @@ export interface LoginOptions {
 	verbose?: boolean
 }
 
-export interface LoginResult {
-	success: boolean
-	error?: string
-	userId?: string
-	orgId?: string | null
-}
+export type LoginResult =
+	| {
+			success: true
+			token: string
+	  }
+	| {
+			success: false
+			error: string
+	  }
 
 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}`)
 	}
 
+	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.
 	const tokenPromise = new Promise<{ token: string; state: string }>((resolve, reject) => {
 		const server = http.createServer((req, res) => {
 			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 token = url.searchParams.get("token")
 				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) {
-					const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=error-in-callback`)
-					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.
+					sendJsonResponse(400, { success: false, error })
 					res.on("close", () => {
 						server.close()
 						reject(new Error(error))
 					})
 				} else if (!token) {
-					const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=missing-token`)
-					errorUrl.searchParams.set("message", "Missing token in callback")
-					res.writeHead(302, { Location: errorUrl.toString() })
-					res.end()
+					sendJsonResponse(400, { success: false, error: "Missing token in callback" })
 					res.on("close", () => {
 						server.close()
 						reject(new Error("Missing token in callback"))
 					})
 				} else if (receivedState !== state) {
-					const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=invalid-state-parameter`)
-					errorUrl.searchParams.set("message", "Invalid state parameter (possible CSRF attack)")
-					res.writeHead(302, { Location: errorUrl.toString() })
-					res.end()
+					sendJsonResponse(400, { success: false, error: "Invalid state parameter" })
 					res.on("close", () => {
 						server.close()
 						reject(new Error("Invalid state parameter"))
 					})
 				} else {
-					res.writeHead(302, { Location: `${AUTH_BASE_URL}/cli/sign-in?success=true` })
-					res.end()
+					sendJsonResponse(200, { success: true })
 					res.on("close", () => {
 						server.close()
 						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"))
 		}, timeout)
 
-		server.on("listening", () => {
-			console.log(`[Auth] Callback server listening on port ${port}`)
-		})
-
 		server.on("close", () => {
-			console.log("[Auth] Callback server closed")
 			clearTimeout(timeoutId)
 		})
 	})
@@ -121,7 +127,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
 		const { token } = await tokenPromise
 		await saveToken(token)
 		console.log("✓ Successfully authenticated!")
-		return { success: true }
+		return { success: true, token }
 	} catch (error) {
 		const message = error instanceof Error ? error.message : String(error)
 		console.error(`✗ Authentication failed: ${message}`)

+ 93 - 0
apps/cli/src/commands/cli/__tests__/run.test.ts

@@ -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 /`")
+	})
+})

+ 49 - 26
apps/cli/src/commands/cli/run.ts

@@ -28,7 +28,7 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js"
 
 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({
 		info: () => {},
 		warn: () => {},
@@ -36,34 +36,60 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
 		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
 
+	let rooToken = await loadToken()
+	const settings = await loadSettings()
+
 	const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
-	const isTuiEnabled = flagOptions.tui && isTuiSupported
-	const rooToken = await loadToken()
+	const isTuiEnabled = !flagOptions.print && isTuiSupported
+	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 = {
-		mode: flagOptions.mode || DEFAULT_FLAGS.mode,
-		reasoningEffort: flagOptions.reasoningEffort === "unspecified" ? undefined : flagOptions.reasoningEffort,
+		mode: effectiveMode,
+		reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort,
 		user: null,
-		provider: flagOptions.provider ?? (rooToken ? "roo" : "openrouter"),
-		model: flagOptions.model || DEFAULT_FLAGS.model,
-		workspacePath: path.resolve(workspaceArg),
+		provider: effectiveProvider,
+		model: effectiveModel,
+		workspacePath: effectiveWorkspacePath,
 		extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
-		nonInteractive: flagOptions.yes,
+		nonInteractive: effectiveDangerouslySkipPermissions,
 		ephemeral: flagOptions.ephemeral,
 		debug: flagOptions.debug,
-		exitOnComplete: flagOptions.exitOnComplete,
+		exitOnComplete: effectiveExitOnComplete,
 	}
 
 	// Roo Code Cloud Authentication
 
-	if (isTuiEnabled) {
-		let { onboardingProviderChoice } = await loadSettings()
+	if (isOnboardingEnabled) {
+		let { onboardingProviderChoice } = settings
 
 		if (!onboardingProviderChoice) {
-			const result = await runOnboarding()
-			onboardingProviderChoice = result.choice
+			const { choice, token } = await runOnboarding()
+			onboardingProviderChoice = choice
+			rooToken = token ?? null
 		}
 
 		if (onboardingProviderChoice === OnboardingProviderChoice.Roo) {
@@ -139,15 +165,15 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
 	}
 
 	if (!isTuiEnabled) {
-		if (!flagOptions.prompt) {
-			console.error("[CLI] Error: prompt is required in plain text mode")
-			console.error("[CLI] Usage: roo [workspace] -P <prompt> [options]")
-			console.error("[CLI] Use TUI mode (without --no-tui) for interactive input")
+		if (!prompt) {
+			console.error("[CLI] Error: prompt is required in print mode")
+			console.error("[CLI] Usage: roo <prompt> --print [options]")
+			console.error("[CLI] Run without -p for interactive mode")
 			process.exit(1)
 		}
 
-		if (flagOptions.tui) {
-			console.warn("[CLI] TUI disabled (no TTY support), falling back to plain text mode")
+		if (!flagOptions.print) {
+			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(
 				createElement(App, {
 					...extensionHostOptions,
-					initialPrompt: flagOptions.prompt,
+					initialPrompt: prompt,
 					version: VERSION,
 					createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts),
 				}),
@@ -200,12 +226,9 @@ export async function run(workspaceArg: string, flagOptions: FlagOptions) {
 
 		try {
 			await host.activate()
-			await host.runTask(flagOptions.prompt!)
+			await host.runTask(prompt!)
 			await host.dispose()
-
-			if (!flagOptions.waitOnComplete) {
-				process.exit(0)
-			}
+			process.exit(0)
 		} catch (error) {
 			console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
 

+ 12 - 13
apps/cli/src/index.ts

@@ -6,31 +6,30 @@ import { run, login, logout, status } from "@/commands/index.js"
 
 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
-	.argument("[workspace]", "Workspace path to operate in", process.cwd())
-	.option("-P, --prompt <prompt>", "The prompt/task to execute (optional in TUI mode)")
+	.argument("[prompt]", "Your prompt")
+	.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("-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("-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, --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(
 		"-r, --reasoning-effort <effort>",
 		"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
 		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("--no-tui", "Disable TUI, use plain text output")
+	.option("--oneshot", "Exit upon task completion", false)
 	.action(run)
 
 const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud")

+ 236 - 0
apps/cli/src/lib/storage/__tests__/settings.test.ts

@@ -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)
+		})
+	})
+})

+ 7 - 2
apps/cli/src/lib/utils/onboarding.ts

@@ -17,9 +17,14 @@ export async function runOnboarding(): Promise<OnboardingResult> {
 			console.log("")
 
 			if (choice === OnboardingProviderChoice.Roo) {
-				const { success: authenticated } = await login()
+				const result = await login()
 				await saveSettings({ onboardingProviderChoice: choice })
-				resolve({ choice: OnboardingProviderChoice.Roo, authenticated, skipped: false })
+
+				resolve({
+					choice: OnboardingProviderChoice.Roo,
+					token: result.success ? result.token : undefined,
+					skipped: false,
+				})
 			} else {
 				console.log("Using your own API key.")
 				console.log("Set your API key via --api-key or environment variable.")

+ 18 - 5
apps/cli/src/types/types.ts

@@ -18,19 +18,20 @@ export function isSupportedProvider(provider: string): provider is SupportedProv
 export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled"
 
 export type FlagOptions = {
-	prompt?: string
+	promptFile?: string
+	workspace?: string
+	print: boolean
 	extension?: string
 	debug: boolean
 	yes: boolean
+	dangerouslySkipPermissions: boolean
 	apiKey?: string
 	provider?: SupportedProvider
 	model?: string
 	mode?: string
 	reasoningEffort?: ReasoningEffortFlagOptions
-	exitOnComplete: boolean
-	waitOnComplete: boolean
 	ephemeral: boolean
-	tui: boolean
+	oneshot: boolean
 }
 
 export enum OnboardingProviderChoice {
@@ -40,10 +41,22 @@ export enum OnboardingProviderChoice {
 
 export interface OnboardingResult {
 	choice: OnboardingProviderChoice
-	authenticated?: boolean
+	token?: string
 	skipped: boolean
 }
 
 export interface CliSettings {
 	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
 }

+ 19 - 22
apps/cli/src/ui/App.tsx

@@ -68,23 +68,24 @@ export interface TUIAppProps extends ExtensionHostOptions {
 /**
  * Inner App component that uses the terminal size context
  */
-function AppInner({
-	initialPrompt,
-	workspacePath,
-	extensionPath,
-	user,
-	provider,
-	apiKey,
-	model,
-	mode,
-	nonInteractive = false,
-	debug,
-	exitOnComplete,
-	reasoningEffort,
-	ephemeral,
-	version,
-	createExtensionHost,
-}: TUIAppProps) {
+function AppInner({ createExtensionHost, ...extensionHostOptions }: TUIAppProps) {
+	const {
+		initialPrompt,
+		workspacePath,
+		extensionPath,
+		user,
+		provider,
+		apiKey,
+		model,
+		mode,
+		nonInteractive = false,
+		debug,
+		exitOnComplete,
+		reasoningEffort,
+		ephemeral,
+		version,
+	} = extensionHostOptions
+
 	const { exit } = useApp()
 
 	const {
@@ -454,12 +455,8 @@ function AppInner({
 			{/* Header - fixed size */}
 			<Box flexShrink={0}>
 				<Header
-					cwd={workspacePath}
-					user={user}
-					provider={provider}
-					model={model}
+					{...extensionHostOptions}
 					mode={currentMode || mode}
-					reasoningEffort={reasoningEffort}
 					version={version}
 					tokenUsage={tokenUsage}
 					contextWindow={contextWindow}

+ 10 - 11
apps/cli/src/ui/components/Header.tsx

@@ -4,32 +4,27 @@ import { Text, Box } from "ink"
 import type { TokenUsage } from "@roo-code/types"
 
 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 * as theme from "../theme.js"
 
 import MetricsDisplay from "./MetricsDisplay.js"
 
-interface HeaderProps {
-	cwd: string
-	user: User | null
-	provider: string
-	model: string
-	mode: string
-	reasoningEffort?: string
+interface HeaderProps extends ExtensionHostOptions {
 	version: string
 	tokenUsage?: TokenUsage | null
 	contextWindow?: number
 }
 
 function Header({
-	cwd,
+	workspacePath,
 	user,
 	provider,
 	model,
 	mode,
 	reasoningEffort,
+	nonInteractive,
 	version,
 	tokenUsage,
 	contextWindow,
@@ -53,12 +48,16 @@ function Header({
 					<Box flexDirection="column" marginLeft={1} marginTop={1}>
 						{user && <Text color={theme.dimText}>Welcome back, {user.name}</Text>}
 						<Text color={theme.dimText}>
-							cwd: {cwd.startsWith(homeDir) ? cwd.replace(homeDir, "~") : cwd}
+							cwd:{" "}
+							{workspacePath.startsWith(homeDir) ? workspacePath.replace(homeDir, "~") : workspacePath}
 						</Text>
 						<Text color={theme.dimText}>
 							{provider}: {model} [{reasoningEffort}]
 						</Text>
-						<Text color={theme.dimText}>mode: {mode}</Text>
+						<Text color={theme.dimText}>
+							mode: {mode}
+							{nonInteractive && " (YOLO)"}
+						</Text>
 					</Box>
 				</Box>
 			</Box>

+ 9 - 11
packages/evals/src/cli/runTaskInCli.ts

@@ -1,4 +1,3 @@
-import * as fs from "fs"
 import * as path from "path"
 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) => {
 	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 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",
 		"@roo-code/cli",
 		"start",
+		"--prompt-file",
+		promptSourcePath,
+		"--workspace",
+		workspacePath,
 		"--yes",
-		"--exit-on-complete",
 		"--reasoning-effort",
 		"disabled",
-		"--workspace",
-		workspacePath,
+		"--oneshot",
 	]
 
 	if (run.settings?.mode) {
-		cliArgs.push("-M", run.settings.mode)
+		cliArgs.push("--mode", run.settings.mode)
 	}
 
 	if (run.settings?.apiProvider) {
-		cliArgs.push("-p", run.settings.apiProvider)
+		cliArgs.push("--provider", run.settings.apiProvider)
 	}
 
 	const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
 
 	if (modelId) {
-		cliArgs.push("-m", modelId)
+		cliArgs.push("--model", modelId)
 	}
 
-	cliArgs.push(prompt)
-
 	logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`)
-
 	const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() })
 
 	// Buffer for accumulating streaming output until we have complete lines.