UNPKG

hackages

Version:

CLI tool for learning software development concepts through test-driven development

203 lines (202 loc) 11 kB
import { printError, printSuccess, printInfo } from "../utils/console.js"; import { getStoredAuth, writeLearningGoal } from "../services/auth.js"; import { cloneRepositoryTemplate, createExerciseFilesInRepository, createExerciseFiles } from "../services/file-manager.js"; import { getTechnologyConfigWithRepository } from "../services/tech.service.js"; import { config } from "../config/index.js"; import chalk from "chalk"; import axios from "axios"; import path from "path"; import fs from "fs"; /** * Ensures the .hackages directory and learning.json file exist */ function ensureHackagesConfig() { const homeDir = process.env.HOME || process.env.USERPROFILE || ""; const hackagesDir = path.join(homeDir, ".hackages"); const learningJsonPath = path.join(hackagesDir, "learning.json"); // Create .hackages directory if it doesn't exist if (!fs.existsSync(hackagesDir)) { fs.mkdirSync(hackagesDir, { recursive: true }); printInfo("Created .hackages directory"); } // Create learning.json file if it doesn't exist if (!fs.existsSync(learningJsonPath)) { fs.writeFileSync(learningJsonPath, JSON.stringify({}, null, 2)); printInfo("Created learning.json file"); } } /** * Loads an exercise from the Hackages API into the local IDE * @param exerciseId The exercise ID to load */ export async function loadCommand(exerciseId) { try { if (!exerciseId) { printError("❌ Exercise ID is required"); printInfo("Usage: npx hackages -l <exercise-id> or npx hackages load <exercise-id>"); return; } // Ensure .hackages folder and learning.json exist ensureHackagesConfig(); printInfo(`Loading exercise: ${exerciseId}...`); // Try to get stored token (optional - may not exist) const auth = getStoredAuth(); const hasAuth = !!auth?.access_token; let exerciseData; try { // Make API request with or without authentication // Use axios directly to avoid interceptor error messages const response = await axios.get(`${config.apiUrl}/api/learning`, { params: { id: exerciseId }, headers: auth?.access_token ? { Authorization: `Bearer ${auth.access_token}`, } : {}, }); exerciseData = response.data; } catch (error) { // Handle different error responses if (axios.isAxiosError(error) && error.response) { const status = error.response.status; const data = error.response.data; switch (status) { case 401: // Unauthorized - exercise is private and no/invalid token if (hasAuth) { printError("❌ Authentication failed. Your token may have expired."); printInfo("Please login again: npx hackages login"); } else { printError("❌ This exercise is private and requires authentication."); printInfo("Please login: npx hackages login"); } return; case 403: // Forbidden - user doesn't have access to this private exercise printError("❌ You don't have permission to access this exercise."); printInfo("This exercise is private and you don't own it."); return; case 404: // Not found printError(`❌ Exercise not found: ${exerciseId}`); printInfo("Please check the exercise ID and try again."); return; default: const errorMessage = data?.error?.message || data?.message || "An error occurred"; printError(`❌ Failed to load exercise: ${errorMessage}`); return; } } // Network or other errors printError(`❌ Failed to load exercise: ${error}`); printInfo("Please check your internet connection and try again."); return; } // Successfully loaded exercise printSuccess(`Exercise loaded successfully!`); // Determine file extensions and templates based on selectedTech const selectedTech = exerciseData.selectedTech || "JavaScript"; const techLower = selectedTech.toLowerCase(); // Normalize technology name let normalizedTech = "javascript"; if (techLower === "typescript" || techLower === "ts") { normalizedTech = "typescript"; } else if (techLower === "javascript" || techLower === "js") { normalizedTech = "javascript"; } // Get technology configuration with repository templates const techConfig = getTechnologyConfigWithRepository(normalizedTech); // Prepare exercise content // Use implementationTemplate if provided, otherwise it will be generated by createExerciseFiles const exerciseContent = { testContent: exerciseData.exercise.testContent, srcContent: exerciseData.exercise.implementationTemplate || "", // Will be generated if empty }; // Create a safe exercise name from the topic const exerciseName = exerciseData.topic.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '-').toLowerCase(); let exerciseFiles; let repoPath = null; try { // Try to clone repository template if available if (techConfig.repository) { repoPath = cloneRepositoryTemplate(techConfig, exerciseName); exerciseFiles = createExerciseFilesInRepository(exerciseData.topic, exerciseContent, techConfig, repoPath, 1 // needs to be dynamic ); // If implementationTemplate is provided, overwrite the generated template if (exerciseData.exercise.implementationTemplate) { const srcFilePath = path.join(repoPath, "src", `exercise-1${techConfig.srcExt}`); fs.writeFileSync(srcFilePath, exerciseData.exercise.implementationTemplate); printInfo(`Using provided implementation template`); } } else { // Fallback to creating files in current directory printInfo("No repository template available, creating files in current directory..."); exerciseFiles = createExerciseFiles(exerciseData.topic, exerciseContent, techConfig); // If implementationTemplate is provided, overwrite the generated template if (exerciseData.exercise.implementationTemplate) { fs.writeFileSync(exerciseFiles.srcFilePath, exerciseData.exercise.implementationTemplate); printInfo(`Using provided implementation template`); } } // Create learning goal and save it const learningGoal = { id: exerciseData.id || exerciseId, topic: exerciseData.topic, selectedTech: selectedTech, skillLevel: "Intermediate", // Default, can be updated later }; writeLearningGoal(learningGoal); // Success message console.log("\n" + chalk.bold.green("Exercise loaded successfully!")); console.log(chalk.cyan("=".repeat(50))); if (repoPath) { const dirName = path.basename(repoPath); console.log(chalk.bold("Repository cloned:")); console.log(chalk.green(` ✓ ${dirName}/`), chalk.dim("(Your exercise directory)")); console.log(chalk.green(` ✓ ${dirName}/tests/exercise-1.spec${techConfig.extension.replace('.spec', '')}`), chalk.dim("(Your test cases)")); console.log(chalk.green(` ✓ ${dirName}/src/exercise-1${techConfig.srcExt}`), chalk.dim("(Your implementation file)")); console.log(chalk.green(` ✓ ${dirName}/instructions/exercise-1.mdx`), chalk.dim("(Exercise instructions)")); console.log("\n" + chalk.bold.yellow("Next steps:")); console.log(chalk.white(`1. cd ${dirName} (navigate to your exercise directory)`)); console.log(chalk.white("2. Read the instructions and test file to understand what to implement")); console.log(chalk.white(`3. Open src/exercise-1${techConfig.srcExt} and start coding`)); console.log(chalk.white("4. Run 'npx hackages test' to validate your implementation")); console.log(chalk.white("5. Run 'npx hackages review' to get feedback and recommendations for next steps")); } else { console.log(chalk.bold("Files created:")); console.log(chalk.green(` ✓ tests/${exerciseFiles.testFileName}`), chalk.dim("(Test cases)")); console.log(chalk.green(` ✓ src/${exerciseFiles.srcFileName}`), chalk.dim("(Implementation file)")); console.log("\n" + chalk.bold.yellow("Next steps:")); console.log(chalk.white("1. Read the test file to understand what to implement")); console.log(chalk.white(`2. Open src/${exerciseFiles.srcFileName} and start coding`)); console.log(chalk.white("3. Run 'npx hackages test' to validate your implementation")); console.log(chalk.white("4. Run 'npx hackages review' to get feedback and recommendations for next steps")); } } catch (error) { printError(`❌ Failed to create exercise: ${error}`); // Fallback to creating files in current directory printInfo("Falling back to creating files in current directory..."); exerciseFiles = createExerciseFiles(exerciseData.topic, exerciseContent, techConfig); // If implementationTemplate is provided, overwrite the generated template if (exerciseData.exercise.implementationTemplate) { fs.writeFileSync(exerciseFiles.srcFilePath, exerciseData.exercise.implementationTemplate); printInfo(`Using provided implementation template`); } // Still save the learning goal const learningGoal = { id: exerciseData.id || exerciseId, topic: exerciseData.topic, selectedTech: selectedTech, skillLevel: "Intermediate", }; writeLearningGoal(learningGoal); } } catch (error) { printError(`❌ Error loading exercise: ${error}`); } }