diff --git a/extensions/cli/src/onboarding.test.ts b/extensions/cli/src/onboarding.test.ts index 036d56097e4..0a3dc0c0467 100644 --- a/extensions/cli/src/onboarding.test.ts +++ b/extensions/cli/src/onboarding.test.ts @@ -162,6 +162,130 @@ name: "Incomplete Config" }); }); +describe("onboarding local config handling", () => { + let tempDir: string; + let originalContinueGlobalDir: string | undefined; + let originalNodeEnv: string | undefined; + let originalCi: string | undefined; + let originalVitest: string | undefined; + let originalGithubActions: string | undefined; + let originalIsTTY: boolean; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "continue-home-")); + originalContinueGlobalDir = process.env.CONTINUE_GLOBAL_DIR; + originalNodeEnv = process.env.NODE_ENV; + originalCi = process.env.CI; + originalVitest = process.env.VITEST; + originalGithubActions = process.env.GITHUB_ACTIONS; + originalIsTTY = process.stdin.isTTY; + + process.env.CONTINUE_GLOBAL_DIR = tempDir; + vi.resetModules(); + vi.doMock("./auth/workos.js", () => ({ + login: vi.fn(), + })); + vi.doMock("./config.js", () => ({ + getApiClient: vi.fn(() => ({})), + })); + vi.doMock("./configLoader.js", () => ({ + loadConfiguration: vi.fn(), + })); + }); + + afterEach(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 100, + }); + } + + if (originalContinueGlobalDir === undefined) { + delete process.env.CONTINUE_GLOBAL_DIR; + } else { + process.env.CONTINUE_GLOBAL_DIR = originalContinueGlobalDir; + } + + if (originalNodeEnv === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = originalNodeEnv; + } + + if (originalCi === undefined) { + delete process.env.CI; + } else { + process.env.CI = originalCi; + } + + if (originalVitest === undefined) { + delete process.env.VITEST; + } else { + process.env.VITEST = originalVitest; + } + + if (originalGithubActions === undefined) { + delete process.env.GITHUB_ACTIONS; + } else { + process.env.GITHUB_ACTIONS = originalGithubActions; + } + + process.stdin.isTTY = originalIsTTY; + + vi.doUnmock("./auth/workos.js"); + vi.doUnmock("./config.js"); + vi.doUnmock("./configLoader.js"); + vi.doUnmock("./util/prompt.js"); + vi.resetModules(); + }); + + test("should skip interactive onboarding when default config.yaml exists", async () => { + fs.writeFileSync(path.join(tempDir, "config.yaml"), "name: Local Config\n"); + + delete process.env.NODE_ENV; + delete process.env.CI; + delete process.env.VITEST; + delete process.env.GITHUB_ACTIONS; + process.stdin.isTTY = true; + + const questionWithChoices = vi + .fn() + .mockRejectedValue(new Error("prompted")); + vi.doMock("./util/prompt.js", () => ({ + question: vi.fn(), + questionWithChoices, + })); + + const { runOnboardingFlow } = await import("./onboarding.js"); + + await expect(runOnboardingFlow(undefined)).resolves.toBe(false); + expect(questionWithChoices).not.toHaveBeenCalled(); + }); + + test("should mark onboarding complete after a successful --config load", async () => { + const configPath = path.join(tempDir, "custom-config.yaml"); + const flagPath = path.join(tempDir, ".onboarding_complete"); + const loadConfiguration = vi.fn().mockResolvedValue({ + config: { name: "Custom Config" }, + source: { path: configPath, type: "cli-flag" }, + }); + + vi.doMock("./configLoader.js", () => ({ + loadConfiguration, + })); + + const { initializeWithOnboarding } = await import("./onboarding.js"); + + await initializeWithOnboarding(null, configPath); + + expect(loadConfiguration).toHaveBeenCalledOnce(); + expect(fs.existsSync(flagPath)).toBe(true); + }); +}); + // Separate describe block with its own mocking for BEDROCK tests describe("CONTINUE_USE_BEDROCK environment variable", () => { const mockConsoleLog = vi.fn(); diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index 2a8162284de..448c53819ec 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -56,7 +56,12 @@ export async function runOnboardingFlow( return false; } - // Step 2: Check for CONTINUE_USE_BEDROCK environment variable first (before test env check) + // Step 2: Existing local config means the user is already configured + if (fs.existsSync(CONFIG_PATH)) { + return false; + } + + // Step 3: 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)"), @@ -64,7 +69,7 @@ export async function runOnboardingFlow( return true; } - // Step 3: Check if we're in a test/CI environment - if so, skip interactive prompts + // Step 4: 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" || @@ -85,7 +90,7 @@ export async function runOnboardingFlow( return false; } - // Step 4: Present user with two options + // Step 5: 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")); @@ -156,6 +161,12 @@ export async function initializeWithOnboarding( `Failed to load config from "${configPath}": ${errorMessage}`, ); } + + if (firstTime && configPath) { + await markOnboardingComplete(); + } + + return; } if (!firstTime) return;