fix(init): Ensure hosted mode option available by creating .taskmasterconfig early

- Added ensureConfigFileExists() to create default config if missing
- Call early in init flows before gateway check - Preserve email from initializeUser()
- Add comprehensive tests
This commit is contained in:
Eyal Toledano
2025-06-05 13:30:14 -04:00
parent 31178e2f43
commit f12fc476d3
3 changed files with 115 additions and 1 deletions

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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);
});
});