mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(cli): improve dev experience and roo provider API key support (#11203)
- Allow --api-key and ROO_API_KEY env var for the roo provider instead of requiring cloud auth token - Switch dev/start scripts to use tsx for running directly from source without building first - Fix path resolution (version.ts, extension.ts, extension-host.ts) to work from both source and bundled locations - Disable debug log file (~/.roo/cli-debug.log) unless --debug is passed - Update README with complete env var table and dev workflow docs Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1da2b1c457
commit
6e56619417
8 changed files with 170 additions and 40 deletions
|
|
@ -177,13 +177,14 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
|
|||
|
||||
The CLI will look for API keys in environment variables if not provided via `--api-key`:
|
||||
|
||||
| Provider | Environment Variable |
|
||||
| ------------- | -------------------- |
|
||||
| anthropic | `ANTHROPIC_API_KEY` |
|
||||
| openai | `OPENAI_API_KEY` |
|
||||
| openrouter | `OPENROUTER_API_KEY` |
|
||||
| google/gemini | `GOOGLE_API_KEY` |
|
||||
| ... | ... |
|
||||
| Provider | Environment Variable |
|
||||
| ----------------- | --------------------------- |
|
||||
| roo | `ROO_API_KEY` |
|
||||
| anthropic | `ANTHROPIC_API_KEY` |
|
||||
| openai-native | `OPENAI_API_KEY` |
|
||||
| openrouter | `OPENROUTER_API_KEY` |
|
||||
| gemini | `GOOGLE_API_KEY` |
|
||||
| vercel-ai-gateway | `VERCEL_AI_GATEWAY_API_KEY` |
|
||||
|
||||
**Authentication Environment Variables:**
|
||||
|
||||
|
|
@ -233,8 +234,8 @@ The CLI will look for API keys in environment variables if not provided via `--a
|
|||
## Development
|
||||
|
||||
```bash
|
||||
# Watch mode for development
|
||||
pnpm dev
|
||||
# Run directly from source (no build required)
|
||||
pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
|
@ -246,6 +247,12 @@ pnpm check-types
|
|||
pnpm lint
|
||||
```
|
||||
|
||||
By default the `start` script points `ROO_CODE_PROVIDER_URL` at `http://localhost:8080/proxy` for local development. To point at the production API instead, override the environment variable:
|
||||
|
||||
```bash
|
||||
ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
Official releases are created via the GitHub Actions workflow at `.github/workflows/cli-release.yml`.
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
"build": "tsup",
|
||||
"build:extension": "pnpm --filter roo-cline bundle",
|
||||
"build:all": "pnpm --filter roo-cline bundle && tsup",
|
||||
"dev": "tsup --watch",
|
||||
"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",
|
||||
"dev": "tsx src/index.ts",
|
||||
"start": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy tsx src/index.ts",
|
||||
"start:production": "node dist/index.js",
|
||||
"build:local": "scripts/build.sh",
|
||||
"clean": "rimraf dist .turbo"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type {
|
|||
WebviewMessage,
|
||||
} from "@roo-code/types"
|
||||
import { createVSCodeAPI, IExtensionHost, ExtensionHostEventMap, setRuntimeConfigValues } from "@roo-code/vscode-shim"
|
||||
import { DebugLogger } from "@roo-code/core/cli"
|
||||
import { DebugLogger, setDebugLogEnabled } from "@roo-code/core/cli"
|
||||
|
||||
import type { SupportedProvider } from "@/types/index.js"
|
||||
import type { User } from "@/lib/sdk/index.js"
|
||||
|
|
@ -43,10 +43,25 @@ const cliLogger = new DebugLogger("CLI")
|
|||
|
||||
// Get the CLI package root directory (for finding node_modules/@vscode/ripgrep)
|
||||
// When running from a release tarball, ROO_CLI_ROOT is set by the wrapper script.
|
||||
// In development, we fall back to calculating from __dirname.
|
||||
// After bundling with tsup, the code is in dist/index.js (flat), so we go up one level.
|
||||
// In development, we fall back to finding the CLI package root by walking up to package.json.
|
||||
// This works whether running from dist/ (bundled) or src/agent/ (tsx dev).
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || path.resolve(__dirname, "..")
|
||||
|
||||
function findCliPackageRoot(): string {
|
||||
let dir = __dirname
|
||||
|
||||
while (dir !== path.dirname(dir)) {
|
||||
if (fs.existsSync(path.join(dir, "package.json"))) {
|
||||
return dir
|
||||
}
|
||||
|
||||
dir = path.dirname(dir)
|
||||
}
|
||||
|
||||
return path.resolve(__dirname, "..")
|
||||
}
|
||||
|
||||
const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || findCliPackageRoot()
|
||||
|
||||
export interface ExtensionHostOptions {
|
||||
mode: string
|
||||
|
|
@ -154,6 +169,11 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
|
||||
this.options = options
|
||||
|
||||
// Enable file-based debug logging only when --debug is passed.
|
||||
if (options.debug) {
|
||||
setDebugLogEnabled(true)
|
||||
}
|
||||
|
||||
// Set up quiet mode early, before any extension code runs.
|
||||
// This suppresses console output from the extension during load.
|
||||
this.setupQuietMode()
|
||||
|
|
|
|||
|
|
@ -112,15 +112,18 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
extensionHostOptions.apiKey = rooToken
|
||||
extensionHostOptions.user = me.user
|
||||
} catch {
|
||||
console.error("[CLI] Your Roo Code Router token is not valid.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
process.exit(1)
|
||||
// If an explicit API key was provided via flag or env var, fall through
|
||||
// to the general API key resolution below instead of exiting.
|
||||
if (!flagOptions.apiKey && !getApiKeyFromEnv(extensionHostOptions.provider)) {
|
||||
console.error("[CLI] Your Roo Code Router token is not valid.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
console.error("[CLI] Or use --api-key or set ROO_API_KEY to provide your own API key.")
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("[CLI] Your Roo Code Router token is missing.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
process.exit(1)
|
||||
}
|
||||
// If no rooToken, fall through to the general API key resolution below
|
||||
// which will check flagOptions.apiKey and ROO_API_KEY env var.
|
||||
}
|
||||
|
||||
// Validations
|
||||
|
|
|
|||
|
|
@ -21,9 +21,26 @@ describe("getDefaultExtensionPath", () => {
|
|||
|
||||
it("should return monorepo path when extension.js exists there", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
// Walk-up: dist/ has no package.json, apps/cli/ does
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
const s = String(p)
|
||||
|
||||
if (s === path.join(mockDirname, "package.json")) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (s === path.join("/test/apps/cli", "package.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (s === path.join(expectedMonorepoPath, "extension.js")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
|
|
@ -33,9 +50,18 @@ describe("getDefaultExtensionPath", () => {
|
|||
|
||||
it("should return package path when extension.js does not exist in monorepo path", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedPackagePath = path.resolve(mockDirname, "../extension")
|
||||
const expectedPackagePath = path.resolve("/test/apps/cli", "extension")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
// Walk-up finds package.json at apps/cli/, but no extension.js in monorepo path
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
const s = String(p)
|
||||
|
||||
if (s === path.join("/test/apps/cli", "package.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
|
|
@ -43,12 +69,45 @@ describe("getDefaultExtensionPath", () => {
|
|||
})
|
||||
|
||||
it("should check monorepo path first", () => {
|
||||
const mockDirname = "/some/path"
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
const s = String(p)
|
||||
|
||||
if (s === path.join("/test/apps/cli", "package.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
getDefaultExtensionPath(mockDirname)
|
||||
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
|
||||
it("should work when called from source directory (tsx dev)", () => {
|
||||
const mockDirname = "/test/apps/cli/src/commands/cli"
|
||||
const expectedMonorepoPath = path.resolve("/test/apps/cli", "../../src/dist")
|
||||
|
||||
// Walk-up: no package.json in src subdirs, found at apps/cli/
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
const s = String(p)
|
||||
|
||||
if (s === path.join("/test/apps/cli", "package.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (s === path.join(expectedMonorepoPath, "extension.js")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedMonorepoPath)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,17 +17,26 @@ export function getDefaultExtensionPath(dirname: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
// __dirname is apps/cli/dist when bundled
|
||||
// The extension is at src/dist (relative to monorepo root)
|
||||
// So from apps/cli/dist, we need to go ../../../src/dist
|
||||
const monorepoPath = path.resolve(dirname, "../../../src/dist")
|
||||
// Find the CLI package root (apps/cli) by walking up to the nearest package.json.
|
||||
// This works whether called from dist/ (bundled) or src/commands/cli/ (tsx dev).
|
||||
let packageRoot = dirname
|
||||
|
||||
while (packageRoot !== path.dirname(packageRoot)) {
|
||||
if (fs.existsSync(path.join(packageRoot, "package.json"))) {
|
||||
break
|
||||
}
|
||||
|
||||
packageRoot = path.dirname(packageRoot)
|
||||
}
|
||||
|
||||
// The extension is at ../../src/dist relative to apps/cli (monorepo/src/dist)
|
||||
const monorepoPath = path.resolve(packageRoot, "../../src/dist")
|
||||
|
||||
// Try monorepo path first (for development)
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
|
||||
// Fallback: when installed via curl script, extension is at ../extension
|
||||
const packagePath = path.resolve(dirname, "../extension")
|
||||
// Fallback: when installed via curl script, extension is at apps/cli/extension
|
||||
const packagePath = path.resolve(packageRoot, "extension")
|
||||
return packagePath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,24 @@
|
|||
import { createRequire } from "module"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const packageJson = require("../package.json")
|
||||
// Walk up from the current file to find the nearest package.json.
|
||||
// This works whether running from source (tsx src/lib/utils/) or bundle (dist/).
|
||||
function findVersion(): string {
|
||||
let dir = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export const VERSION = packageJson.version
|
||||
while (dir !== path.dirname(dir)) {
|
||||
const candidate = path.join(dir, "package.json")
|
||||
|
||||
if (fs.existsSync(candidate)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(candidate, "utf-8"))
|
||||
return packageJson.version
|
||||
}
|
||||
|
||||
dir = path.dirname(dir)
|
||||
}
|
||||
|
||||
return "0.0.0"
|
||||
}
|
||||
|
||||
export const VERSION = findVersion()
|
||||
|
|
|
|||
|
|
@ -21,11 +21,25 @@ import * as os from "os"
|
|||
|
||||
const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")
|
||||
|
||||
let debugLogEnabled = false
|
||||
|
||||
/**
|
||||
* Enable or disable file-based debug logging.
|
||||
* Logging is disabled by default and should only be enabled in dev/debug mode.
|
||||
*/
|
||||
export function setDebugLogEnabled(enabled: boolean): void {
|
||||
debugLogEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple file-based debug log function.
|
||||
* Writes timestamped entries to ~/.roo/cli-debug.log
|
||||
* Only writes when enabled via setDebugLogEnabled(true).
|
||||
*/
|
||||
export function debugLog(message: string, data?: unknown): void {
|
||||
if (!debugLogEnabled) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const logDir = path.dirname(DEBUG_LOG_PATH)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue