hackages
Version:
CLI tool for learning software development concepts through test-driven development
203 lines (202 loc) • 11 kB
JavaScript
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}`);
}
}