Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down Expand Up @@ -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
Expand All @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 24 additions & 4 deletions apps/cli/src/agent/extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
17 changes: 10 additions & 7 deletions apps/cli/src/commands/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 66 additions & 7 deletions apps/cli/src/lib/utils/__tests__/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -33,22 +50,64 @@ 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)

expect(result).toBe(expectedPackagePath)
})

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)
})
})
23 changes: 16 additions & 7 deletions apps/cli/src/lib/utils/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 22 additions & 4 deletions apps/cli/src/lib/utils/version.ts
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 14 additions & 0 deletions packages/core/src/debug-log/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading