hackages
Version:
CLI tool for learning software development concepts through test-driven development
208 lines (207 loc) • 8.81 kB
JavaScript
import fs from "fs";
import chalk from "chalk";
import { apiClient } from "../services/api.js";
import { getLearningInformation, getLatestSourceFilePath, } from "../services/file-manager.js";
import { printError, printHeader } from "../utils/console.js";
const HINT_INTERVAL = 10000; // 10 seconds between hint requests
let lastContent = "";
let hasChanges = false;
let hintTimer = null;
let isRequestingHint = false;
// Hackages brand colors
const primary = chalk.rgb(34, 211, 238); // Cyan/turquoise
const primaryBg = chalk.bgRgb(34, 211, 238); // Cyan background
const tertiary = chalk.rgb(22, 78, 99); // Dark teal
const tertiaryBg = chalk.bgRgb(22, 78, 99); // Dark teal background
/**
* Displays a hint in the terminal with tutor-style formatting
*/
function displayHint(hint) {
console.log("\n");
console.log(tertiaryBg.white.bold(" "));
console.log(tertiaryBg.white.bold(" HACKAGES AI SAYS... "));
console.log(tertiaryBg.white.bold(" "));
console.log("");
// Process the hint with enhanced formatting
const lines = hint.split("\n");
for (const line of lines) {
// Highlight key phrases
let formattedLine = line
// Bold important keywords
.replace(/\*\*(.*?)\*\*/g, (_, p1) => chalk.bold.white(p1))
// Inline code
.replace(/`([^`]+)`/g, (_, p1) => tertiaryBg.white(` ${p1} `))
// Tips and suggestions
.replace(/^(Tip:|Hint:|Suggestion:|Try:|Consider:)/gi, (match) => primary.bold(`> ${match}`))
// Warnings
.replace(/^(Warning:|Careful:|Watch out:|Note:)/gi, (match) => chalk.red.bold(`! ${match}`))
// Success indicators
.replace(/^(Great!|Good job!|Well done!|Correct!)/gi, (match) => chalk.green.bold(`+ ${match}`));
// Headers
if (line.startsWith("###")) {
console.log(primary.bold("\n" + line.replace(/^###\s*/, " -> ")));
}
else if (line.startsWith("##")) {
console.log(primary.bold("\n" + line.replace(/^##\s*/, ">> ")));
}
else if (line.startsWith("#")) {
console.log(primary.bold("\n" + line.replace(/^#\s*/, "# ")));
}
// Lists
else if (line.match(/^\s*[-*]\s/)) {
console.log(chalk.white(" " + line.replace(/^\s*[-*]\s/, primary(" - "))));
}
else if (line.match(/^\s*\d+\.\s/)) {
console.log(chalk.white(" " + line.replace(/^(\s*\d+\.)\s/, primary("$1 "))));
}
// Code blocks
else if (line.trim().startsWith("```")) {
console.log(tertiary(" ─────────────────────────────────"));
}
// Empty lines
else if (line.trim() === "") {
console.log("");
}
// Regular text
else {
console.log(chalk.white(" " + formattedLine));
}
}
console.log("");
console.log(tertiary(" ─────────────────────────────────────────────────"));
console.log(tertiary(` Last updated: ${new Date().toLocaleTimeString()}`));
console.log(primary(" > ") + tertiary("Keep coding! Hackages AI will check again in 10 seconds."));
console.log("");
}
/**
* Displays the watching status bar
*/
function displayWatchingStatus(fileName) {
console.log(primaryBg.black.bold(" "));
console.log(primaryBg.black.bold(" HACKAGES AI - TUTOR MODE ACTIVE "));
console.log(primaryBg.black.bold(" "));
console.log("");
console.log(primary(" [*] ") + chalk.white("Watching: ") + primary.bold(fileName));
console.log(primary(" [*] ") + chalk.white("Hint interval: ") + primary.bold("Every 10 seconds") + chalk.white(" (when code changes)"));
console.log(primary(" [*] ") + chalk.white("Stop: ") + chalk.red.bold("Ctrl+C"));
console.log("");
console.log(tertiary(" ─────────────────────────────────────────────────"));
console.log("");
}
/**
* Fetches a hint from the API based on current implementation
*/
async function fetchHint(learningGoalId, implementation) {
if (isRequestingHint)
return;
isRequestingHint = true;
try {
console.log(primary("\n [~] ") + tertiary("Analyzing your code..."));
const response = await apiClient.learning.getHint(learningGoalId, implementation);
if (response.hint) {
displayHint(response.hint);
}
else {
console.log(tertiary("\n [i] No specific hints at this time. Keep going!"));
}
}
catch (error) {
printError(`Failed to fetch hint: ${error}`);
}
finally {
isRequestingHint = false;
}
}
/**
* Checks for changes and requests hint if needed
*/
async function checkAndFetchHint(filePath, learningGoalId) {
if (!hasChanges || isRequestingHint) {
return;
}
// Reset change flag before fetching
hasChanges = false;
try {
const currentContent = fs.readFileSync(filePath, "utf-8");
await fetchHint(learningGoalId, currentContent);
}
catch (error) {
printError(`Error reading file: ${error}`);
}
}
/**
* Handles file change events
*/
function handleFileChange(filePath) {
try {
const currentContent = fs.readFileSync(filePath, "utf-8");
// Only mark as changed if content actually differs
if (currentContent !== lastContent) {
hasChanges = true;
lastContent = currentContent;
console.log(tertiary(` [${new Date().toLocaleTimeString()}] `) + primary("Code change detected, will analyze soon..."));
}
}
catch (error) {
// File might be in the middle of being saved, ignore
}
}
export async function hintCommand() {
try {
console.clear();
printHeader("Hackages AI - Interactive Learning Mode");
console.log("");
// Get learning information
const learningInfo = getLearningInformation();
if (!learningInfo) {
console.log(chalk.red.bold("\n [x] No active learning session found."));
console.log(chalk.white(" Please start with ") + primary("'hackages'") + chalk.white(" first.\n"));
return;
}
// Get the source file path
const sourceFile = getLatestSourceFilePath();
if (!sourceFile) {
console.log(chalk.red.bold("\n [x] No source file found."));
console.log(chalk.white(" Please make sure you have files in the ") + primary("src/") + chalk.white(" directory.\n"));
return;
}
const { filePath, fileName } = sourceFile;
// Display watching status
displayWatchingStatus(fileName);
// Read initial content
lastContent = fs.readFileSync(filePath, "utf-8");
// Fetch initial hint
console.log(primary(" [>] ") + chalk.white("Fetching your first hint..."));
await fetchHint(learningInfo.id, lastContent);
// Watch for file changes
const watcher = fs.watch(filePath, (eventType) => {
if (eventType === "change") {
handleFileChange(filePath);
}
});
// Set up interval to check for changes and fetch hints
hintTimer = setInterval(() => {
checkAndFetchHint(filePath, learningInfo.id);
}, HINT_INTERVAL);
// Handle process termination
process.on("SIGINT", () => {
console.log("\n");
console.log(tertiaryBg.white.bold(" "));
console.log(tertiaryBg.white.bold(" HACKAGES AI SESSION ENDED "));
console.log(tertiaryBg.white.bold(" "));
console.log("");
console.log(chalk.white(" Great learning session! Keep practicing and come back anytime."));
console.log(tertiary(" Run ") + primary("hackages hint") + tertiary(" to start another session.\n"));
watcher.close();
if (hintTimer) {
clearInterval(hintTimer);
}
process.exit(0);
});
// Keep the process running
await new Promise(() => { });
}
catch (error) {
printError(`Error in hint command: ${error}`);
}
}