diff --git a/scripts/init.js b/scripts/init.js index 72e4a4c9..053955ae 100755 --- a/scripts/init.js +++ b/scripts/init.js @@ -34,6 +34,7 @@ import { initializeBYOKUser, initializeHostedUser, } from "./modules/user-management.js"; +import { ensureConfigFileExists } from "./modules/config-manager.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -391,6 +392,9 @@ async function initializeProject(options = {}) { let userSetupResult = null; let isGatewayAvailable = false; + // Ensure .taskmasterconfig exists before checking gateway availability + ensureConfigFileExists(process.cwd()); + // Try to initialize user, but don't throw errors if it fails try { userSetupResult = await initializeUser(process.cwd()); @@ -452,6 +456,9 @@ async function initializeProject(options = {}) { let userSetupResult = null; let isGatewayAvailable = false; + // Ensure .taskmasterconfig exists before checking gateway availability + ensureConfigFileExists(process.cwd()); + try { userSetupResult = await initializeUser(process.cwd()); if (userSetupResult.success) { @@ -866,7 +873,12 @@ function configureTaskmasterConfig( // Store account-specific configuration config.account.mode = selectedMode; config.account.userId = userId || null; - config.account.email = gatewayRegistration?.email || ""; + + // Only set email if not already present (initializeUser may have already set it) + if (!config.account.email) { + config.account.email = gatewayRegistration?.email || ""; + } + config.account.telemetryEnabled = selectedMode === "hosted"; // Store remaining global config items diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index 24842f32..0d1c55fd 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -808,6 +808,57 @@ function getMode(explicitRoot = null) { return config.account?.mode || "byok"; } +/** + * Ensures that the .taskmasterconfig file exists, creating it with defaults if it doesn't. + * This is called early in initialization to prevent chicken-and-egg problems. + * @param {string|null} explicitRoot - Optional explicit path to the project root + * @returns {boolean} True if file exists or was created successfully, false otherwise + */ +function ensureConfigFileExists(explicitRoot = null) { + // ---> Determine root path reliably (following existing pattern) <--- + let rootPath = explicitRoot; + if (explicitRoot === null || explicitRoot === undefined) { + // Logic matching _loadAndValidateConfig and other functions + const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot *** + if (!foundRoot) { + console.warn( + chalk.yellow( + "Warning: Could not determine project root for config file creation." + ) + ); + return false; + } + rootPath = foundRoot; + } + // ---> End determine root path logic <--- + + const configPath = path.join(rootPath, CONFIG_FILE_NAME); + + // If file already exists, we're good + if (fs.existsSync(configPath)) { + return true; + } + + try { + // Create the default config file (following writeConfig pattern) + fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2)); + console.log(chalk.blue(`ℹ️ Created default .taskmasterconfig file`)); + + // Clear any cached config to ensure fresh load + loadedConfig = null; + loadedConfigRoot = null; + + return true; + } catch (error) { + console.error( + chalk.red( + `Error creating default .taskmasterconfig file: ${error.message}` + ) + ); + return false; + } +} + export { // Core config access getConfig, @@ -856,4 +907,6 @@ export { getTelemetryEnabled, getUserEmail, getMode, + // New function + ensureConfigFileExists, }; diff --git a/tests/unit/config-manager.test.js b/tests/unit/config-manager.test.js index ede4af75..4a4c2233 100644 --- a/tests/unit/config-manager.test.js +++ b/tests/unit/config-manager.test.js @@ -641,3 +641,52 @@ describe("getAllProviders", () => { // Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation. // If similar setter functions exist, add tests for them following the writeConfig pattern. + +describe("ensureConfigFileExists", () => { + it("should create .taskmasterconfig file if it doesn't exist", () => { + // Override the default fs mocks for this test + fsExistsSyncSpy.mockReturnValue(false); + fsWriteFileSyncSpy.mockImplementation(() => {}); // Success, no throw + + const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT); + + expect(result).toBe(true); + expect(fsWriteFileSyncSpy).toHaveBeenCalledWith( + MOCK_CONFIG_PATH, + JSON.stringify(DEFAULT_CONFIG, null, 2) + ); + }); + + it("should return true if .taskmasterconfig file already exists", () => { + // Mock file exists (this is the default, but let's be explicit) + fsExistsSyncSpy.mockReturnValue(true); + + const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT); + + expect(result).toBe(true); + expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); + }); + + it("should return false if project root cannot be determined", () => { + // Override the default findProjectRoot mock to return null for this test + mockFindProjectRoot.mockReturnValue(null); + + const result = configManager.ensureConfigFileExists(); // No explicitRoot provided + + expect(result).toBe(false); + expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); + }); + + it("should handle write errors gracefully", () => { + // Mock file doesn't exist + fsExistsSyncSpy.mockReturnValue(false); + // Mock write operation to throw error + fsWriteFileSyncSpy.mockImplementation(() => { + throw new Error("Permission denied"); + }); + + const result = configManager.ensureConfigFileExists(MOCK_PROJECT_ROOT); + + expect(result).toBe(false); + }); +});