mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Support a --workspace flag
This commit is contained in:
parent
80de137735
commit
1f08401523
5 changed files with 350 additions and 11 deletions
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))
|
||||
|
||||
export async function run(prompt: string | undefined, flagOptions: FlagOptions) {
|
||||
export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
|
||||
setLogger({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
|
|
@ -36,22 +36,45 @@ export async function run(prompt: string | undefined, 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.print && isTuiSupported
|
||||
let rooToken = await loadToken()
|
||||
const isOnboardingEnabled = isTuiEnabled && !rooToken && !flagOptions.provider
|
||||
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 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: process.cwd(),
|
||||
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.print,
|
||||
|
|
@ -60,7 +83,7 @@ export async function run(prompt: string | undefined, flagOptions: FlagOptions)
|
|||
// Roo Code Cloud Authentication
|
||||
|
||||
if (isOnboardingEnabled) {
|
||||
let { onboardingProviderChoice } = await loadSettings()
|
||||
let { onboardingProviderChoice } = settings
|
||||
|
||||
if (!onboardingProviderChoice) {
|
||||
const { choice, token } = await runOnboarding()
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ program
|
|||
|
||||
program
|
||||
.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", 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("--provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)
|
||||
|
|
|
|||
208
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
208
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -18,10 +18,13 @@ export function isSupportedProvider(provider: string): provider is SupportedProv
|
|||
export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
|
||||
export type FlagOptions = {
|
||||
promptFile?: string
|
||||
workspace?: string
|
||||
print: boolean
|
||||
extension?: string
|
||||
debug: boolean
|
||||
yes: boolean
|
||||
dangerouslySkipPermissions: boolean
|
||||
apiKey?: string
|
||||
provider?: SupportedProvider
|
||||
model?: string
|
||||
|
|
@ -43,4 +46,14 @@ export interface OnboardingResult {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue