diff --git a/.vscode/launch.json b/.vscode/launch.json index 22349e3a84c..bce0483d6e2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -159,7 +159,37 @@ "!**/node_modules/**" ], "preLaunchTask": "cli:build-and-watch-logs", - "cwd": "${workspaceFolder}/extensions/cli" + "cwd": "${workspaceFolder}/manual-testing-sandbox" + }, + { + "name": "Debug CLI - AWS Bedrock", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/extensions/cli/dist/cn.js", + "args": [], + "runtimeArgs": ["--enable-source-maps"], + "env": { + "NODE_ENV": "development", + "CONTINUE_USE_BEDROCK": "1" + // "CONTINUE_API_BASE": "http://localhost:3001/", + // "WORKOS_CLIENT_ID": "client_01J0FW6XCPMJMQ3CG51RB4HBZQ", + // "HUB_URL": "http://localhost:3000" + }, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": ["/**"], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/extensions/cli/dist/**/*.js"], + "sourceMapPathOverrides": { + "../src/*": "${workspaceFolder}/extensions/cli/src/*", + "../../core/*": "${workspaceFolder}/core/*" + }, + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!**/node_modules/**" + ], + "preLaunchTask": "cli:build-and-watch-logs", + "cwd": "${workspaceFolder}/manual-testing-sandbox" }, { "name": "Debug CLI Tests", diff --git a/extensions/cli/spec/onboarding.md b/extensions/cli/spec/onboarding.md index 523b9067e1f..58c65132292 100644 --- a/extensions/cli/spec/onboarding.md +++ b/extensions/cli/spec/onboarding.md @@ -7,7 +7,8 @@ When a user first runs `cn` in interactive mode, they will be taken through "onb **The onboarding flow runs when the user hasn't completed onboarding before, regardless of whether they have a valid config.yaml file.** 1. If the --config flag is provided, load this config -2. Otherwise, we will present the user with two options: +2. If the CONTINUE_USE_BEDROCK environment variable is set to "1", automatically use AWS Bedrock configuration and skip interactive prompts +3. Present the user with available options: - Log in with Continue: log them in, which will automatically create their assistant and then we can load the first assistant from the first org - Enter your Anthropic API key: let them enter the key, and then either create a ~/.continue/config.yaml with the following contents OR update the existing config.yaml to add the model @@ -23,7 +24,26 @@ When a user first runs `cn` in interactive mode, they will be taken through "onb ANTHROPIC_API_KEY: ``` -When something in the onboarding flow is done automatically, we should tell the user what happened. + When CONTINUE_USE_BEDROCK=1 is detected, it will use AWS Bedrock configuration. The user must have AWS credentials configured through the standard AWS credential chain (AWS CLI, environment variables, IAM roles, etc.). + +When something in the onboarding flow is done automatically, we should tell the user what happened. For example, when CONTINUE_USE_BEDROCK=1 is detected, the CLI displays: "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)" + +### AWS Bedrock Environment Variable + +Users can bypass the interactive onboarding menu by setting the `CONTINUE_USE_BEDROCK` environment variable to "1": + +```bash +export CONTINUE_USE_BEDROCK=1 +cn +``` + +This will: + +- Skip the interactive onboarding prompts +- Automatically configure the CLI to use AWS Bedrock +- Require that AWS credentials are already configured through the standard AWS credential chain +- Display a confirmation message to the user +- Mark onboarding as completed ## Normal flow diff --git a/extensions/cli/src/onboarding.test.ts b/extensions/cli/src/onboarding.test.ts index c31bc1608d9..1864e636854 100644 --- a/extensions/cli/src/onboarding.test.ts +++ b/extensions/cli/src/onboarding.test.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { describe, expect, test, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import type { AuthConfig } from "./auth/workos.js"; import { runNormalFlow } from "./onboarding.js"; @@ -160,3 +160,93 @@ name: "Incomplete Config" } }); }); + +// Separate describe block with its own mocking for BEDROCK tests +describe("CONTINUE_USE_BEDROCK environment variable", () => { + const mockConsoleLog = vi.fn(); + let mockAuthConfig: AuthConfig; + const originalEnv = process.env.CONTINUE_USE_BEDROCK; + + // Mock initialize for these tests only + const mockInitialize = vi.fn().mockResolvedValue({ + config: { name: "test-config", models: [], rules: [] }, + llmApi: {}, + model: { name: "test-model" }, + mcpService: {}, + apiClient: {}, + }); + + beforeEach(() => { + mockConsoleLog.mockClear(); + mockInitialize.mockClear(); + + // Spy on console.log for these tests + vi.spyOn(console, "log").mockImplementation(mockConsoleLog); + + // Mock the config module + vi.doMock("./config.js", () => ({ initialize: mockInitialize })); + + mockAuthConfig = { + userId: "test-user", + userEmail: "test@example.com", + accessToken: "test-token", + refreshToken: "test-refresh", + expiresAt: Date.now() + 3600000, + organizationId: "test-org", + }; + }); + + afterEach(() => { + if (originalEnv) { + process.env.CONTINUE_USE_BEDROCK = originalEnv; + } else { + delete process.env.CONTINUE_USE_BEDROCK; + } + vi.restoreAllMocks(); + vi.doUnmock("./config.js"); + }); + + test("should bypass interactive options when CONTINUE_USE_BEDROCK=1", async () => { + process.env.CONTINUE_USE_BEDROCK = "1"; + + // Re-import to get the mocked version + vi.resetModules(); + const { runOnboardingFlow } = await import("./onboarding.js"); + + const result = await runOnboardingFlow(undefined, mockAuthConfig); + + expect(result.wasOnboarded).toBe(true); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining( + "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)", + ), + ); + }); + + test("should not bypass when CONTINUE_USE_BEDROCK is not '1'", async () => { + process.env.CONTINUE_USE_BEDROCK = "0"; + + // Re-import to get the mocked version + vi.resetModules(); + const { runOnboardingFlow } = await import("./onboarding.js"); + + // Mock non-interactive environment to avoid hanging + const originalIsTTY = process.stdin.isTTY; + process.stdin.isTTY = false; + + try { + await runOnboardingFlow(undefined, mockAuthConfig); + + // Verify the Bedrock message was NOT called by checking all calls + const allCalls = mockConsoleLog.mock.calls.flat(); + const hasBedrockMessage = allCalls.some((call) => + String(call).includes( + "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)", + ), + ); + expect(hasBedrockMessage).toBe(false); + } finally { + process.stdin.isTTY = originalIsTTY; + } + }); +}); diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index fa5b094b42f..57ed8155f47 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -59,7 +59,7 @@ export async function createOrUpdateConfig(apiKey: string): Promise { fs.writeFileSync(CONFIG_PATH, updatedContent); } -async function runOnboardingFlow( +export async function runOnboardingFlow( configPath: string | undefined, authConfig: AuthConfig, ): Promise { @@ -69,7 +69,16 @@ async function runOnboardingFlow( return { ...result, wasOnboarded: false }; } - // Step 2: Check if we're in a test/CI environment - if so, skip interactive prompts + // Step 2: Check for CONTINUE_USE_BEDROCK environment variable first (before test env check) + if (process.env.CONTINUE_USE_BEDROCK === "1") { + console.log( + chalk.blue("✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)"), + ); + const result = await initialize(authConfig, CONFIG_PATH); + return { ...result, wasOnboarded: true }; + } + + // Step 3: Check if we're in a test/CI environment - if so, skip interactive prompts const isTestEnv = process.env.NODE_ENV === "test" || process.env.CI === "true" || @@ -92,7 +101,7 @@ async function runOnboardingFlow( return { ...result, wasOnboarded: false }; } - // Step 3: Present user with two options + // Step 4: Present user with two options console.log(chalk.yellow("How do you want to get started?")); console.log(chalk.white("1. ⏩ Log in with Continue")); console.log(chalk.white("2. 🔑 Enter your Anthropic API key")); @@ -129,7 +138,7 @@ async function runOnboardingFlow( const result = await initialize(authConfig, CONFIG_PATH); return { ...result, wasOnboarded: true }; } else { - throw new Error("Invalid choice. Please select 1 or 2."); + throw new Error(`Invalid choice. Please select "1" or "2"`); } }