From afb6a6eff5be004f927d83af1713fee31f67593a Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:37:16 +0530 Subject: [PATCH 1/4] remove onboarding result from being returned --- extensions/cli/src/services/index.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index a8d209d88d8..2f3c7ce4f4c 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -23,7 +23,6 @@ import { ConfigServiceState, SERVICE_NAMES, ServiceInitOptions, - ServiceInitResult, WorkflowServiceState, } from "./types.js"; import { UpdateService } from "./UpdateService.js"; @@ -48,23 +47,19 @@ const systemMessageService = new SystemMessageService(); * Initialize all services and register them with the service container * Handles onboarding internally for TUI mode unless skipOnboarding is true */ -export async function initializeServices( - initOptions: ServiceInitOptions = {}, -): Promise { +export async function initializeServices(initOptions: ServiceInitOptions = {}) { logger.debug("Initializing service registry"); - let wasOnboarded = false; const commandOptions = initOptions.options || {}; // Handle onboarding for TUI mode (headless: false) unless explicitly skipped if (!initOptions.headless && !initOptions.skipOnboarding) { const authConfig = loadAuthConfig(); - const onboardingResult = await initializeWithOnboarding( + await initializeWithOnboarding( authConfig, commandOptions.config, commandOptions.rule, ); - wasOnboarded = onboardingResult.wasOnboarded; } // Handle ANTHROPIC_API_KEY in headless mode when no config path is provided @@ -287,8 +282,6 @@ export async function initializeServices( await serviceContainer.initializeAll(); logger.debug("Service registry initialized"); - - return { wasOnboarded }; } /** From 401578a9f2bdfdf77d2655c030f0866bbb527cf3 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:38:23 +0530 Subject: [PATCH 2/4] run normal initialization flow on parallel --- extensions/cli/src/onboarding.ts | 37 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index 0108baa905c..9bbe55f20b9 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -280,24 +280,35 @@ export async function initializeWithOnboarding( authConfig: AuthConfig, configPath: string | undefined, rules?: string[], -): Promise { +) { const firstTime = await isFirstTime(); - let result: OnboardingResult; - if (firstTime) { - result = await runOnboardingFlow(configPath, authConfig); - if (result.wasOnboarded) { + const onboardingResult = await runOnboardingFlow(configPath, authConfig); + if (onboardingResult.wasOnboarded) { await markOnboardingComplete(); } + // Inject rules into the config if provided (for onboarding flow which doesn't handle rules directly) + if (rules && rules.length > 0 && !onboardingResult.wasOnboarded) { + onboardingResult.config = await injectRulesIntoConfig( + onboardingResult.config, + rules, + ); + } } else { - result = await runNormalFlow(authConfig, configPath, rules); - } - - // Inject rules into the config if provided (for onboarding flow which doesn't handle rules directly) - if (rules && rules.length > 0 && !result.wasOnboarded) { - result.config = await injectRulesIntoConfig(result.config, rules); + // when running normal flow, initialize (remote) config asynchronously + void (async () => { + const onboardingResult = await runNormalFlow( + authConfig, + configPath, + rules, + ); + if (rules && rules.length > 0) { + onboardingResult.config = await injectRulesIntoConfig( + onboardingResult.config, + rules, + ); + } + })(); } - - return result; } From 452da6eaf0600818a5e8fbc2d4a9def348daad85 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:11:59 +0530 Subject: [PATCH 3/4] remove rule injection during onboarding --- extensions/cli/src/config.ts | 23 --- extensions/cli/src/onboarding.ts | 201 ++------------------------- extensions/cli/src/services/index.ts | 8 +- 3 files changed, 16 insertions(+), 216 deletions(-) diff --git a/extensions/cli/src/config.ts b/extensions/cli/src/config.ts index 6c97767bcc3..90734d83569 100644 --- a/extensions/cli/src/config.ts +++ b/extensions/cli/src/config.ts @@ -7,7 +7,6 @@ import { import { Configuration, DefaultApi, - DefaultApiInterface, } from "@continuedev/sdk/dist/api/dist/index.js"; import { @@ -15,9 +14,7 @@ import { getAccessToken, getOrganizationId, } from "./auth/workos.js"; -import { loadConfiguration } from "./configLoader.js"; import { env } from "./env.js"; -import { MCPService } from "./services/MCPService.js"; /** * Creates an LLM API instance from a ModelConfig and auth configuration @@ -96,23 +93,3 @@ export function getApiClient( }), ); } - -export async function initialize( - authConfig: AuthConfig, - configPath: string | undefined, -): Promise<{ - config: AssistantUnrolled; - llmApi: BaseLlmApi; - model: ModelConfig; - mcpService: MCPService; - apiClient: DefaultApiInterface; -}> { - const apiClient = getApiClient(authConfig?.accessToken); - const result = await loadConfiguration(authConfig, configPath, apiClient); - const config = result.config; - const [llmApi, model] = getLlmApi(config, authConfig); - const mcpService = new MCPService(); - await mcpService.initialize(config, false); - - return { config, llmApi, model, mcpService, apiClient }; -} diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index 9bbe55f20b9..f1d4b8c420d 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -1,16 +1,10 @@ import * as fs from "fs"; import * as path from "path"; -import { AssistantUnrolled, ModelConfig } from "@continuedev/config-yaml"; -import { BaseLlmApi } from "@continuedev/openai-adapters"; -import { DefaultApiInterface } from "@continuedev/sdk/dist/api/dist/index.js"; import chalk from "chalk"; -import { AuthConfig, isAuthenticated, login } from "./auth/workos.js"; -import { initialize } from "./config.js"; +import { login } from "./auth/workos.js"; import { env } from "./env.js"; -import { processRule } from "./hubLoader.js"; -import { MCPService } from "./services/MCPService.js"; import { getApiKeyValidationError, isValidAnthropicApiKey, @@ -20,15 +14,6 @@ import { updateAnthropicModelInYaml } from "./util/yamlConfigUpdater.js"; const CONFIG_PATH = path.join(env.continueHome, "config.yaml"); -export interface OnboardingResult { - config: AssistantUnrolled; - llmApi: BaseLlmApi; - model: ModelConfig; - mcpService: MCPService; - apiClient: DefaultApiInterface; - wasOnboarded: boolean; -} - export async function checkHasAcceptableModel( configPath: string, ): Promise { @@ -61,12 +46,10 @@ export async function createOrUpdateConfig(apiKey: string): Promise { export async function runOnboardingFlow( configPath: string | undefined, - authConfig: AuthConfig, -): Promise { +): Promise { // Step 1: Check if --config flag is provided if (configPath !== undefined) { - const result = await initialize(authConfig, configPath); - return { ...result, wasOnboarded: false }; + return false; } // Step 2: Check for CONTINUE_USE_BEDROCK environment variable first (before test env check) @@ -74,8 +57,7 @@ export async function runOnboardingFlow( console.log( chalk.blue("✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)"), ); - const result = await initialize(authConfig, CONFIG_PATH); - return { ...result, wasOnboarded: true }; + return true; } // Step 3: Check if we're in a test/CI environment - if so, skip interactive prompts @@ -92,13 +74,11 @@ export async function runOnboardingFlow( console.log(chalk.blue("✓ Using ANTHROPIC_API_KEY from environment")); await createOrUpdateConfig(process.env.ANTHROPIC_API_KEY); console.log(chalk.gray(` Config saved to: ${CONFIG_PATH}`)); - const result = await initialize(authConfig, CONFIG_PATH); - return { ...result, wasOnboarded: false }; + return false; } // Otherwise return a minimal working configuration - const result = await initialize(authConfig, undefined); - return { ...result, wasOnboarded: false }; + return false; } // Step 4: Present user with two options @@ -114,13 +94,8 @@ export async function runOnboardingFlow( ); if (choice === "1" || choice === "") { - const newAuthConfig = await login(); - - const { ensureOrganization } = await import("./auth/workos.js"); - const finalAuthConfig = await ensureOrganization(newAuthConfig); - - const result = await initialize(finalAuthConfig, undefined); - return { ...result, wasOnboarded: true }; + await login(); + return true; } else if (choice === "2") { const apiKey = await question( chalk.white("\nEnter your Anthropic API key: "), @@ -135,89 +110,12 @@ export async function runOnboardingFlow( chalk.green(`✓ Config file updated successfully at ${CONFIG_PATH}`), ); - const result = await initialize(authConfig, CONFIG_PATH); - return { ...result, wasOnboarded: true }; + return true; } else { throw new Error(`Invalid choice. Please select "1" or "2"`); } } -export async function runNormalFlow( - authConfig: AuthConfig, - configPath?: string, - rules?: string[], -): Promise { - // Step 1: Check if --config flag is provided - if (configPath !== undefined) { - // Empty string is invalid and should be treated as an error - if (configPath === "") { - throw new Error( - `Failed to load config from "": Config path cannot be empty`, - ); - } - - try { - const result = await initialize(authConfig, configPath); - // Inject rules into the config if provided - if (rules && rules.length > 0) { - result.config = await injectRulesIntoConfig(result.config, rules); - } - return { ...result, wasOnboarded: false }; - } catch (error) { - // If user explicitly provided --config flag, fail loudly instead of falling back - const errorMessage = - error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to load config from "${configPath}": ${errorMessage}`, - ); - } - } - - // Step 2: If user is logged in, look for first assistant in selected org - if (isAuthenticated()) { - try { - const result = await initialize(authConfig, undefined); - // Inject rules into the config if provided - if (rules && rules.length > 0) { - result.config = await injectRulesIntoConfig(result.config, rules); - } - return { ...result, wasOnboarded: false }; - } catch { - // Silently ignore errors when loading default assistant - } - } - - // Step 3: Look for local ~/.continue/config.yaml - if (fs.existsSync(CONFIG_PATH)) { - try { - const result = await initialize(authConfig, CONFIG_PATH); - // Inject rules into the config if provided - if (rules && rules.length > 0) { - result.config = await injectRulesIntoConfig(result.config, rules); - } - return { ...result, wasOnboarded: false }; - } catch { - console.log(chalk.yellow("⚠ Invalid config file found")); - } - } - - // Step 4: Look for ANTHROPIC_API_KEY in environment - if (process.env.ANTHROPIC_API_KEY) { - console.log(chalk.blue("✓ Using ANTHROPIC_API_KEY from environment")); - await createOrUpdateConfig(process.env.ANTHROPIC_API_KEY); - console.log(chalk.gray(` Config saved to: ${CONFIG_PATH}`)); - const result = await initialize(authConfig, CONFIG_PATH); - // Inject rules into the config if provided - if (rules && rules.length > 0) { - result.config = await injectRulesIntoConfig(result.config, rules); - } - return { ...result, wasOnboarded: false }; - } - - // Step 5: Fall back to onboarding flow - return runOnboardingFlow(configPath, authConfig); -} - export async function isFirstTime(): Promise { return !fs.existsSync(path.join(env.continueHome, ".onboarding_complete")); } @@ -233,82 +131,13 @@ export async function markOnboardingComplete(): Promise { fs.writeFileSync(flagPath, new Date().toISOString()); } -/** - * Process rules and inject them into the assistant config - * @param config - The assistant config to modify - * @param rules - Array of rule specifications to process and inject - * @returns The modified config with injected rules - */ -async function injectRulesIntoConfig( - config: AssistantUnrolled, - rules: string[], -): Promise { - if (!rules || rules.length === 0) { - return config; - } - - const processedRules: string[] = []; - for (const ruleSpec of rules) { - try { - const processedRule = await processRule(ruleSpec); - processedRules.push(processedRule); - } catch (error: any) { - console.warn( - chalk.yellow( - `Warning: Failed to process rule "${ruleSpec}": ${error.message}`, - ), - ); - } - } - - if (processedRules.length === 0) { - return config; - } - - // Clone the config to avoid mutating the original - const modifiedConfig = { ...config }; - - // Add processed rules to the config's rules array - // Each processed rule is a string, which is a valid Rule type - const existingRules = modifiedConfig.rules || []; - modifiedConfig.rules = [...existingRules, ...processedRules]; - - return modifiedConfig; -} - -export async function initializeWithOnboarding( - authConfig: AuthConfig, - configPath: string | undefined, - rules?: string[], -) { +export async function initializeWithOnboarding(configPath: string | undefined) { const firstTime = await isFirstTime(); - if (firstTime) { - const onboardingResult = await runOnboardingFlow(configPath, authConfig); - if (onboardingResult.wasOnboarded) { - await markOnboardingComplete(); - } - // Inject rules into the config if provided (for onboarding flow which doesn't handle rules directly) - if (rules && rules.length > 0 && !onboardingResult.wasOnboarded) { - onboardingResult.config = await injectRulesIntoConfig( - onboardingResult.config, - rules, - ); - } - } else { - // when running normal flow, initialize (remote) config asynchronously - void (async () => { - const onboardingResult = await runNormalFlow( - authConfig, - configPath, - rules, - ); - if (rules && rules.length > 0) { - onboardingResult.config = await injectRulesIntoConfig( - onboardingResult.config, - rules, - ); - } - })(); + if (!firstTime) return; + + const wasOnboarded = await runOnboardingFlow(configPath); + if (wasOnboarded) { + await markOnboardingComplete(); } } diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index 2f3c7ce4f4c..73395155f6b 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -1,4 +1,3 @@ -import { loadAuthConfig } from "../auth/workos.js"; import { initializeWithOnboarding } from "../onboarding.js"; import { logger } from "../util/logger.js"; @@ -54,12 +53,7 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { // Handle onboarding for TUI mode (headless: false) unless explicitly skipped if (!initOptions.headless && !initOptions.skipOnboarding) { - const authConfig = loadAuthConfig(); - await initializeWithOnboarding( - authConfig, - commandOptions.config, - commandOptions.rule, - ); + await initializeWithOnboarding(commandOptions.config); } // Handle ANTHROPIC_API_KEY in headless mode when no config path is provided From 55638464c5b969441007dbcee57740526870d7cb Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:19:51 +0530 Subject: [PATCH 4/4] fix tests --- extensions/cli/src/onboarding.test.ts | 44 +++++++++++++++------------ extensions/cli/src/onboarding.ts | 24 +++++++++++++-- extensions/cli/src/services/index.ts | 4 ++- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/extensions/cli/src/onboarding.test.ts b/extensions/cli/src/onboarding.test.ts index 1864e636854..1dee85c8dd8 100644 --- a/extensions/cli/src/onboarding.test.ts +++ b/extensions/cli/src/onboarding.test.ts @@ -5,7 +5,7 @@ import * as path from "path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import type { AuthConfig } from "./auth/workos.js"; -import { runNormalFlow } from "./onboarding.js"; +import { initializeWithOnboarding } from "./onboarding.js"; describe("onboarding config flag handling", () => { let tempDir: string; @@ -40,7 +40,9 @@ describe("onboarding config flag handling", () => { expect(fs.existsSync(configPath)).toBe(false); // Should throw an error that mentions both the path and the failure - await expect(runNormalFlow(mockAuthConfig, configPath)).rejects.toThrow( + await expect( + initializeWithOnboarding(mockAuthConfig, configPath), + ).rejects.toThrow( /Failed to load config from ".*non-existent\.yaml": .*ENOENT/, ); }); @@ -64,9 +66,9 @@ models: expect(fs.existsSync(configPath)).toBe(true); // Should throw an error mentioning the path and failure to load - await expect(runNormalFlow(mockAuthConfig, configPath)).rejects.toThrow( - /Failed to load config from ".*malformed\.yaml": .+/, - ); + await expect( + initializeWithOnboarding(mockAuthConfig, configPath), + ).rejects.toThrow(/Failed to load config from ".*malformed\.yaml": .+/); }); test("should fail loudly when --config points to file with missing required fields", async () => { @@ -85,9 +87,9 @@ name: "Incomplete Config" expect(fs.existsSync(configPath)).toBe(true); // Should throw with our specific error format and include path - await expect(runNormalFlow(mockAuthConfig, configPath)).rejects.toThrow( - /^Failed to load config from ".*": .+/, - ); + await expect( + initializeWithOnboarding(mockAuthConfig, configPath), + ).rejects.toThrow(/^Failed to load config from ".*": .+/); }); test("should handle different config path formats with proper error messages", async () => { @@ -99,16 +101,18 @@ name: "Incomplete Config" ]; for (const configPath of testPaths) { - await expect(runNormalFlow(mockAuthConfig, configPath)).rejects.toThrow( - /Failed to load config from ".*": .+/, - ); + await expect( + initializeWithOnboarding(mockAuthConfig, configPath), + ).rejects.toThrow(/Failed to load config from ".*": .+/); } }); test("should handle empty string config path", async () => { // Empty string should be treated differently from undefined // Note: empty string triggers onboarding flow, but should still fail in our error format - await expect(runNormalFlow(mockAuthConfig, "")).rejects.toThrow(); + await expect( + initializeWithOnboarding(mockAuthConfig, ""), + ).rejects.toThrow(); }); test("should not fall back to default config when explicit config fails", async () => { @@ -117,7 +121,7 @@ name: "Incomplete Config" // Create a bad config file fs.writeFileSync(configPath, "invalid: yaml: content: ["); - const promise = runNormalFlow(mockAuthConfig, configPath); + const promise = initializeWithOnboarding(mockAuthConfig, configPath); await expect(promise).rejects.toThrow(); @@ -144,13 +148,13 @@ name: "Incomplete Config" fs.writeFileSync(badConfigPath, "invalid yaml ["); // Case 1: Explicit --config that fails should throw our specific error - await expect(runNormalFlow(mockAuthConfig, badConfigPath)).rejects.toThrow( - /^Failed to load config from "/, - ); + await expect( + initializeWithOnboarding(mockAuthConfig, badConfigPath), + ).rejects.toThrow(/^Failed to load config from "/); // Case 2: No explicit config should follow different logic try { - await runNormalFlow(mockAuthConfig, undefined); + await initializeWithOnboarding(mockAuthConfig, undefined); // If it succeeds, that's fine - the point is it's different behavior } catch (error) { const errorMessage = @@ -213,9 +217,9 @@ describe("CONTINUE_USE_BEDROCK environment variable", () => { vi.resetModules(); const { runOnboardingFlow } = await import("./onboarding.js"); - const result = await runOnboardingFlow(undefined, mockAuthConfig); + const result = await runOnboardingFlow(undefined); - expect(result.wasOnboarded).toBe(true); + expect(result).toBe(true); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining( "✓ Using AWS Bedrock (CONTINUE_USE_BEDROCK detected)", @@ -235,7 +239,7 @@ describe("CONTINUE_USE_BEDROCK environment variable", () => { process.stdin.isTTY = false; try { - await runOnboardingFlow(undefined, mockAuthConfig); + await runOnboardingFlow(undefined); // Verify the Bedrock message was NOT called by checking all calls const allCalls = mockConsoleLog.mock.calls.flat(); diff --git a/extensions/cli/src/onboarding.ts b/extensions/cli/src/onboarding.ts index f1d4b8c420d..a8f78059164 100644 --- a/extensions/cli/src/onboarding.ts +++ b/extensions/cli/src/onboarding.ts @@ -3,7 +3,9 @@ import * as path from "path"; import chalk from "chalk"; -import { login } from "./auth/workos.js"; +import { AuthConfig, login } from "./auth/workos.js"; +import { getApiClient } from "./config.js"; +import { loadConfiguration } from "./configLoader.js"; import { env } from "./env.js"; import { getApiKeyValidationError, @@ -131,9 +133,27 @@ export async function markOnboardingComplete(): Promise { fs.writeFileSync(flagPath, new Date().toISOString()); } -export async function initializeWithOnboarding(configPath: string | undefined) { +export async function initializeWithOnboarding( + authConfig: AuthConfig, + configPath: string | undefined, +) { const firstTime = await isFirstTime(); + if (configPath !== undefined) { + // throw an early error is configPath is invalid or has errors + try { + await loadConfiguration( + authConfig, + configPath, + getApiClient(authConfig?.accessToken), + ); + } catch (errorMessage) { + throw new Error( + `Failed to load config from "${configPath}": ${errorMessage}`, + ); + } + } + if (!firstTime) return; const wasOnboarded = await runOnboardingFlow(configPath); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index 73395155f6b..9ff58f36530 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -1,3 +1,4 @@ +import { loadAuthConfig } from "../auth/workos.js"; import { initializeWithOnboarding } from "../onboarding.js"; import { logger } from "../util/logger.js"; @@ -53,7 +54,8 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { // Handle onboarding for TUI mode (headless: false) unless explicitly skipped if (!initOptions.headless && !initOptions.skipOnboarding) { - await initializeWithOnboarding(commandOptions.config); + const authConfig = loadAuthConfig(); + await initializeWithOnboarding(authConfig, commandOptions.config); } // Handle ANTHROPIC_API_KEY in headless mode when no config path is provided