diff --git a/apps/cli/README.md b/apps/cli/README.md index 6165448e71d..da04d21e277 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -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`. diff --git a/apps/cli/package.json b/apps/cli/package.json index abea4771e0f..52e8d31559f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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" diff --git a/apps/cli/src/agent/extension-host.ts b/apps/cli/src/agent/extension-host.ts index e1f55a30d1f..51c06185074 100644 --- a/apps/cli/src/agent/extension-host.ts +++ b/apps/cli/src/agent/extension-host.ts @@ -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() diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 663ed5cf750..b4235338e7d 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -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 diff --git a/apps/cli/src/lib/utils/__tests__/extension.test.ts b/apps/cli/src/lib/utils/__tests__/extension.test.ts index 31fdbe87f00..4b4a2db5850 100644 --- a/apps/cli/src/lib/utils/__tests__/extension.test.ts +++ b/apps/cli/src/lib/utils/__tests__/extension.test.ts @@ -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") + + // 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) - vi.mocked(fs.existsSync).mockReturnValue(false) + 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) + }) }) diff --git a/apps/cli/src/lib/utils/extension.ts b/apps/cli/src/lib/utils/extension.ts index 904940ec004..f49b2df8651 100644 --- a/apps/cli/src/lib/utils/extension.ts +++ b/apps/cli/src/lib/utils/extension.ts @@ -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 } diff --git a/apps/cli/src/lib/utils/version.ts b/apps/cli/src/lib/utils/version.ts index e4f2ce59b21..c599963bdc6 100644 --- a/apps/cli/src/lib/utils/version.ts +++ b/apps/cli/src/lib/utils/version.ts @@ -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() diff --git a/packages/core/src/debug-log/index.ts b/packages/core/src/debug-log/index.ts index 48fb22c4fab..f157d327343 100644 --- a/packages/core/src/debug-log/index.ts +++ b/packages/core/src/debug-log/index.ts @@ -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)