feat: finish bilingual linux learning lab
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
"author": "Codex",
|
||||
"updated": "2026-03-19",
|
||||
"description": "A rebuilt Linux learning path that moves from command basics to real operations troubleshooting with sandbox exercises.",
|
||||
"module_count": 8,
|
||||
"total_lessons": 24,
|
||||
"total_exercises": 82,
|
||||
"module_count": 10,
|
||||
"total_lessons": 30,
|
||||
"total_exercises": 106,
|
||||
"pedagogy": "learning-first",
|
||||
"orientation": "ops-workflow"
|
||||
},
|
||||
@@ -581,6 +581,148 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module_9_web_delivery",
|
||||
"title": "Module 9: Web delivery and runtime verification",
|
||||
"summary": "Connect service state, sockets, configs, and HTTP requests into a simple web delivery workflow.",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m9_l1_service_to_http",
|
||||
"title": "From service state to HTTP verification",
|
||||
"goal": "Learn how to verify a web service from the process layer up to the HTTP layer.",
|
||||
"why_it_matters": "A web service is not truly healthy until the application responds correctly, not just when the process exists.",
|
||||
"concepts": ["Service state", "Socket readiness", "HTTP verification"],
|
||||
"command": "systemctl / ss / curl",
|
||||
"examples": ["systemctl status nginx", "ss -ltnp", "curl http://127.0.0.1"],
|
||||
"pitfalls": ["Treating a running service as proof of healthy delivery", "Stopping at a listening socket without making a request"],
|
||||
"scenarios": ["A service was restarted after deployment but the site still looks broken", "You need to prove whether the issue is process, port, or response body"],
|
||||
"troubleshooting_flow": ["Check the service state", "Check listening sockets", "Send a real HTTP request", "Use the result to decide whether you need logs next"],
|
||||
"related_commands": ["systemctl", "ss", "curl", "journalctl"],
|
||||
"classic_view": "Healthy delivery means the request path works end to end, not just that one process is running.",
|
||||
"takeaways": ["Check multiple layers", "Do not confuse a listening port with a correct response", "Always include a real request in a delivery check"],
|
||||
"after_class": "Practice saying which layer each command verifies: service, port, or HTTP response.",
|
||||
"exercises": [
|
||||
{ "id": "m9_l1_e1", "type": "operation", "title": "Check nginx service state", "hint": "Run systemctl status nginx.", "success_test": "cmd == 'systemctl status nginx' and 'Active:' in output", "solution": ["systemctl status nginx"], "success_msg": "You verified the service layer." },
|
||||
{ "id": "m9_l1_e2", "type": "operation", "title": "Check listening sockets", "hint": "Run ss -ltnp.", "success_test": "cmd == 'ss -ltnp' and 'LISTEN' in output", "solution": ["ss -ltnp"], "success_msg": "You verified the socket layer." },
|
||||
{ "id": "m9_l1_e3", "type": "operation", "title": "Check the HTTP response", "hint": "Run curl http://127.0.0.1.", "success_test": "cmd == 'curl http://127.0.0.1' and 'hello localhost' in output", "solution": ["curl http://127.0.0.1"], "success_msg": "You verified the HTTP layer." },
|
||||
{ "id": "m9_l1_e4", "type": "scenario", "question": "If systemctl and ss look fine but curl still fails, which layer deserves your attention next?", "answer": "The application response path and logs, because process and port checks alone do not prove healthy delivery." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m9_l2_fetch_and_compare",
|
||||
"title": "Fetch and compare remote content with curl and wget",
|
||||
"goal": "Understand the difference between checking a response and saving an artifact.",
|
||||
"why_it_matters": "Operations work often mixes quick request verification with actual file retrieval during delivery or recovery.",
|
||||
"concepts": ["Response inspection", "Artifact download", "Saved output validation"],
|
||||
"command": "curl / wget",
|
||||
"examples": ["curl http://127.0.0.1", "wget https://example.com/file.tar.gz"],
|
||||
"pitfalls": ["Using wget when you only need a quick response check", "Downloading a file without checking where it was saved"],
|
||||
"scenarios": ["Confirm a page is reachable", "Fetch a package or static asset into a working directory"],
|
||||
"troubleshooting_flow": ["Use curl for quick HTTP inspection", "Use wget when you need a saved file", "Verify the saved path or body immediately"],
|
||||
"related_commands": ["curl", "wget", "ls", "cat"],
|
||||
"classic_view": "Choose between inspect and download deliberately instead of treating all HTTP tools as the same.",
|
||||
"takeaways": ["Use curl for response checks", "Use wget for saved artifacts", "Verify destination and content after retrieval"],
|
||||
"after_class": "Explain why curl and wget answer different operational questions even when they both touch HTTP.",
|
||||
"exercises": [
|
||||
{ "id": "m9_l2_e1", "type": "operation", "title": "Inspect the local web response", "hint": "Run curl http://127.0.0.1.", "success_test": "cmd == 'curl http://127.0.0.1' and 'hello localhost' in output", "solution": ["curl http://127.0.0.1"], "success_msg": "You used curl as a response-checking tool." },
|
||||
{ "id": "m9_l2_e2", "type": "operation", "title": "Simulate downloading an artifact", "hint": "Run wget https://example.com/file.tar.gz.", "success_test": "cmd == 'wget https://example.com/file.tar.gz' and 'saved' in output.lower()", "solution": ["wget https://example.com/file.tar.gz"], "success_msg": "You used wget as an artifact download tool." },
|
||||
{ "id": "m9_l2_e3", "type": "understanding", "question": "Why is curl often the better first tool during a health check?", "answer": "Because it quickly shows the response without introducing file-placement concerns." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m9_l3_config_runtime",
|
||||
"title": "Inspect runtime config clues with cat, grep, and journalctl",
|
||||
"goal": "Correlate config files, log output, and service behavior during a web issue.",
|
||||
"why_it_matters": "Web incidents often require you to connect what the config says with what the logs report.",
|
||||
"concepts": ["Config inspection", "Log correlation", "Runtime mismatch"],
|
||||
"command": "cat / grep / journalctl",
|
||||
"examples": ["cat /etc/nginx.conf", "grep error /var/log/syslog", "journalctl -n 20"],
|
||||
"pitfalls": ["Changing config before reading the current file", "Reading logs without checking the active config context"],
|
||||
"scenarios": ["An app starts but still shows misbehavior", "You suspect a config mismatch or startup error"],
|
||||
"troubleshooting_flow": ["Read the config file", "Search logs for the matching signal", "Compare the config expectation with the runtime evidence"],
|
||||
"related_commands": ["cat", "grep", "journalctl", "systemctl"],
|
||||
"classic_view": "Config and logs make more sense when you read them as one story instead of separate artifacts.",
|
||||
"takeaways": ["Read before changing", "Search logs for matching symptoms", "Use config and logs together"],
|
||||
"after_class": "Practice explaining how one config clue should influence what log pattern you search for.",
|
||||
"exercises": [
|
||||
{ "id": "m9_l3_e1", "type": "operation", "title": "Read nginx.conf", "hint": "Run cat /etc/nginx.conf.", "success_test": "cmd == 'cat /etc/nginx.conf' and 'worker_processes' in output", "solution": ["cat /etc/nginx.conf"], "success_msg": "You inspected the active config file." },
|
||||
{ "id": "m9_l3_e2", "type": "operation", "title": "Search syslog for errors", "hint": "Run grep error /var/log/syslog.", "success_test": "cmd == 'grep error /var/log/syslog' and 'error' in output.lower()", "solution": ["grep error /var/log/syslog"], "success_msg": "You filtered log output down to the error signal." },
|
||||
{ "id": "m9_l3_e3", "type": "operation", "title": "Read recent journal entries", "hint": "Run journalctl -n 20.", "success_test": "cmd == 'journalctl -n 20' and 'nginx' in output.lower()", "solution": ["journalctl -n 20"], "success_msg": "You correlated service behavior with recent log evidence." }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module_10_backup_and_recovery",
|
||||
"title": "Module 10: Backup, recovery, and safe change patterns",
|
||||
"summary": "Practice the habits that make change safer: copy, archive, inspect, restore, and verify.",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m10_l1_backup_patterns",
|
||||
"title": "Create rollback options with cp and tar",
|
||||
"goal": "Understand simple rollback preparation before changing files or directories.",
|
||||
"why_it_matters": "A safe change is easier when you can return to a known-good file or archive quickly.",
|
||||
"concepts": ["Point-in-time copy", "Archive backup", "Rollback preparation"],
|
||||
"command": "cp / tar",
|
||||
"examples": ["cp /etc/hosts /tmp/hosts.rollback", "tar -czf /tmp/etc-snapshot.tar.gz /etc"],
|
||||
"pitfalls": ["Changing a file before creating any rollback artifact", "Treating a backup as valid without verifying it exists"],
|
||||
"scenarios": ["Prepare for a risky config edit", "Bundle a directory tree for later recovery"],
|
||||
"troubleshooting_flow": ["Create a copy or archive first", "Verify the backup exists", "Only then proceed with the risky change"],
|
||||
"related_commands": ["cp", "tar", "ls", "stat"],
|
||||
"classic_view": "Backups are what gives you permission to change safely.",
|
||||
"takeaways": ["Make rollback options first", "Verify that the backup artifact exists", "Choose copy vs archive based on scope"],
|
||||
"after_class": "Before any file edit, say what your rollback object is and where it lives.",
|
||||
"exercises": [
|
||||
{ "id": "m10_l1_e1", "type": "operation", "title": "Create a rollback copy", "hint": "Run cp /etc/hosts /tmp/hosts.rollback.", "success_test": "cmd == 'cp /etc/hosts /tmp/hosts.rollback' and exists('/tmp/hosts.rollback')", "solution": ["cp /etc/hosts /tmp/hosts.rollback"], "success_msg": "You created a rollback copy before change." },
|
||||
{ "id": "m10_l1_e2", "type": "operation", "title": "Create an archive snapshot", "hint": "Run tar -czf /tmp/etc-snapshot.tar.gz /etc.", "success_test": "cmd == 'tar -czf /tmp/etc-snapshot.tar.gz /etc' and exists('/tmp/etc-snapshot.tar.gz')", "solution": ["tar -czf /tmp/etc-snapshot.tar.gz /etc"], "success_msg": "You created an archive-based snapshot." },
|
||||
{ "id": "m10_l1_e3", "type": "scenario", "question": "Why is a verified backup more valuable than just planning to remember the old state?", "answer": "Because rollback depends on an actual artifact, not memory or assumptions." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m10_l2_restore_path",
|
||||
"title": "Restore into a clean path and inspect the result",
|
||||
"goal": "Practice extracting or copying into a clean recovery target instead of overwriting blindly.",
|
||||
"why_it_matters": "Safe recovery often means restoring to a staging path first so you can inspect what you got back.",
|
||||
"concepts": ["Clean restore target", "Artifact extraction", "Verification after recovery"],
|
||||
"command": "mkdir / tar / ls",
|
||||
"examples": ["mkdir -p /tmp/restore/etc", "tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc", "ls /tmp/restore/etc"],
|
||||
"pitfalls": ["Extracting directly into a live path without inspection", "Skipping path validation after a restore"],
|
||||
"scenarios": ["Recover a config tree into a staging directory", "Inspect a restored artifact before using it"],
|
||||
"troubleshooting_flow": ["Create a clean target path", "Extract into the clean path", "List the restored result before using it"],
|
||||
"related_commands": ["mkdir", "tar", "ls", "find"],
|
||||
"classic_view": "Recovery is safer when you restore into a clean inspection space first.",
|
||||
"takeaways": ["Restore into a staging path when possible", "Inspect restored content before reuse", "Use ls or find to verify the recovery target"],
|
||||
"after_class": "Practice saying where you would restore first and why that path is safer than the live target.",
|
||||
"exercises": [
|
||||
{ "id": "m10_l2_e1", "type": "operation", "title": "Create a clean restore path", "hint": "Run mkdir -p /tmp/restore/etc.", "success_test": "cmd == 'mkdir -p /tmp/restore/etc' and exists('/tmp/restore/etc')", "solution": ["mkdir -p /tmp/restore/etc"], "success_msg": "You created a clean restore target." },
|
||||
{ "id": "m10_l2_e2", "type": "operation", "title": "Extract the snapshot into the restore path", "hint": "Run tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc.", "success_test": "cmd == 'tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc' and exists('/tmp/restore/etc')", "solution": ["tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc"], "success_msg": "You restored the archive into a clean path." },
|
||||
{ "id": "m10_l2_e3", "type": "operation", "title": "List the restore target", "hint": "Run ls /tmp/restore/etc.", "success_test": "cmd == 'ls /tmp/restore/etc' and len(output) >= 0", "solution": ["ls /tmp/restore/etc"], "success_msg": "You inspected the restore target after extraction." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m10_l3_change_window",
|
||||
"title": "Plan a safe change window with date, cal, history, and journalctl",
|
||||
"goal": "Think about change timing, traceability, and rollback evidence as part of routine operations work.",
|
||||
"why_it_matters": "A safe operator tracks when a change happened and what happened immediately before or after it.",
|
||||
"concepts": ["Time anchoring", "Change timeline", "Quick rollback evidence"],
|
||||
"command": "date / cal / history / journalctl",
|
||||
"examples": ["date", "cal", "history", "journalctl -n 20"],
|
||||
"pitfalls": ["Making a change without checking maintenance timing", "Losing the command narrative that explains what happened"],
|
||||
"scenarios": ["Prepare for a maintenance window", "Reconstruct the sequence around a recent restart or config change"],
|
||||
"troubleshooting_flow": ["Anchor the time", "Check the calendar or window context", "Review your command narrative", "Read recent logs around the change"],
|
||||
"related_commands": ["date", "cal", "history", "journalctl"],
|
||||
"classic_view": "Operational safety improves when every change has time context, command context, and log context.",
|
||||
"takeaways": ["Time-stamp your changes", "Keep a readable command narrative", "Pair history with logs for recovery thinking"],
|
||||
"after_class": "Practice explaining a change using time, commands, and logs as one connected story.",
|
||||
"exercises": [
|
||||
{ "id": "m10_l3_e1", "type": "operation", "title": "Anchor the current time", "hint": "Run date.", "success_test": "cmd == 'date' and 'CST' in output", "solution": ["date"], "success_msg": "You anchored the current time." },
|
||||
{ "id": "m10_l3_e2", "type": "operation", "title": "Review the calendar context", "hint": "Run cal.", "success_test": "cmd == 'cal' and 'March' in output", "solution": ["cal"], "success_msg": "You reviewed the calendar context." },
|
||||
{ "id": "m10_l3_e3", "type": "operation", "title": "Review your command narrative", "hint": "Run history.", "success_test": "cmd == 'history' and len(output) >= 0", "solution": ["history"], "success_msg": "You reviewed the shell command timeline." },
|
||||
{ "id": "m10_l3_e4", "type": "operation", "title": "Review recent journal lines", "hint": "Run journalctl -n 20.", "success_test": "cmd == 'journalctl -n 20' and 'nginx' in output.lower()", "solution": ["journalctl -n 20"], "success_msg": "You linked the change window to recent service logs." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1079
index.html
1079
index.html
File diff suppressed because it is too large
Load Diff
@@ -81,9 +81,9 @@ SUPPORTED_COMMANDS = {
|
||||
"env", "export", "fdisk", "find", "free", "grep", "head", "history", "id",
|
||||
"ifconfig", "ip", "journalctl", "kill", "last", "less", "ls", "lsof", "mkdir",
|
||||
"more", "mount", "mv", "netstat", "nohup", "passwd", "ping", "pkill", "ps",
|
||||
"pwd", "rm", "sed", "service", "sort", "ss", "stat", "su", "systemctl", "tail",
|
||||
"tar", "top", "touch", "traceroute", "uniq", "uptime", "vi", "vim", "w",
|
||||
"wc", "wget", "whereis", "which", "yum"
|
||||
"pwd", "rm", "rpm", "sed", "service", "sort", "ss", "stat", "su", "systemctl",
|
||||
"tail", "tar", "top", "touch", "traceroute", "uniq", "uptime", "vi", "vim", "w",
|
||||
"wc", "wget", "whereis", "which", "whoami", "yum", "dpkg", "cal"
|
||||
}
|
||||
|
||||
|
||||
|
||||
456
server.py
456
server.py
@@ -106,6 +106,19 @@ def command_label(commands: list[str]) -> str:
|
||||
return " / ".join(commands[:3])
|
||||
|
||||
|
||||
def is_supported_command_token(command: str) -> bool:
|
||||
normalized = command.strip().lower()
|
||||
if not normalized:
|
||||
return False
|
||||
supported = SANDBOX.supported_commands()
|
||||
if normalized in supported:
|
||||
return True
|
||||
if "|" in normalized:
|
||||
segments = [segment.strip() for segment in normalized.split("|") if segment.strip()]
|
||||
return bool(segments) and all(segment in supported for segment in segments)
|
||||
return False
|
||||
|
||||
|
||||
def module_number(module_id: str | None) -> str | None:
|
||||
if not module_id:
|
||||
return None
|
||||
@@ -188,6 +201,439 @@ def default_after_class(commands: list[str]) -> str:
|
||||
return f"Repeat this lesson in the sandbox two or three more times and explain every step out loud using {command_label(commands)}."
|
||||
|
||||
|
||||
COMMAND_CATEGORIES = {
|
||||
"navigation": {"pwd", "ls", "cd", "which", "whereis", "clear"},
|
||||
"filesystem": {"cat", "echo", "mkdir", "touch", "cp", "mv", "rm", "chmod", "stat", "tar"},
|
||||
"text": {"grep", "find", "head", "tail", "sort", "uniq", "cut", "awk", "sed"},
|
||||
"observation": {"ps", "top", "uptime", "free", "df", "du", "lsof", "last", "w", "history", "date", "cal"},
|
||||
"service": {"systemctl", "journalctl", "nohup", "crontab"},
|
||||
"network": {"ip", "ping", "ss", "curl", "wget", "netstat", "traceroute", "dig"},
|
||||
"identity": {"whoami", "id", "env", "export", "alias"},
|
||||
"package": {"apt", "yum", "rpm", "dpkg"},
|
||||
}
|
||||
|
||||
CATEGORY_ROLES = {
|
||||
"navigation": "Use this command family to regain orientation before acting.",
|
||||
"filesystem": "Use this command family to inspect, create, move, or protect files safely.",
|
||||
"text": "Use this command family to filter noisy output into evidence you can act on.",
|
||||
"observation": "Use this command family to understand what the host is doing right now.",
|
||||
"service": "Use this command family to control long-running workloads and read their execution history.",
|
||||
"network": "Use this command family to test reachability, sockets, HTTP responses, and DNS resolution.",
|
||||
"identity": "Use this command family to understand who you are, what your shell knows, and what your environment passes downstream.",
|
||||
"package": "Use this command family to inspect, install, or verify software that powers the host.",
|
||||
"general": "Use the base form first, then add options only when you know what evidence you need.",
|
||||
}
|
||||
|
||||
CATEGORY_EXTENSION_TIPS = {
|
||||
"navigation": [
|
||||
"Repeat the lesson from a second directory and explain why path context changes the next command.",
|
||||
"Practice saying the current directory out loud before every file-changing action.",
|
||||
],
|
||||
"filesystem": [
|
||||
"Run the read-only form first, then explain what would happen if you switched to a state-changing form.",
|
||||
"Create a small backup-and-verify routine so file work becomes reversible instead of risky.",
|
||||
],
|
||||
"text": [
|
||||
"Turn one noisy log or command output into a smaller evidence set with filters and field extraction.",
|
||||
"Compare broad search output with a narrower variant and explain which one is safer under pressure.",
|
||||
],
|
||||
"observation": [
|
||||
"Capture the one output field that matters most before jumping into repair actions.",
|
||||
"Explain how this observation command changes your next diagnostic step instead of stopping at raw output.",
|
||||
],
|
||||
"service": [
|
||||
"Link service state, listening port, and logs into a three-step incident playbook.",
|
||||
"Practice one failed-start scenario and one healthy-state verification sequence.",
|
||||
],
|
||||
"network": [
|
||||
"Distinguish host reachability, open port, HTTP response, and DNS lookup as separate layers.",
|
||||
"Run one command per network layer and explain what each result rules in or rules out.",
|
||||
],
|
||||
"identity": [
|
||||
"Check identity and environment before using sudo, editing config, or launching a process.",
|
||||
"Practice spotting the difference between shell state and system-wide state.",
|
||||
],
|
||||
"package": [
|
||||
"Trace one tool from package name to installed files to executable path.",
|
||||
"Explain how package inspection helps before upgrades, rollbacks, or incident recovery.",
|
||||
],
|
||||
}
|
||||
|
||||
COMMAND_ORIGINS = {
|
||||
"pwd": "pwd is short for 'print working directory'.",
|
||||
"ls": "ls is the compact Unix shorthand for 'list'.",
|
||||
"grep": "grep comes from the old ed command g/re/p, meaning globally search a regular expression and print matches.",
|
||||
"awk": "awk is named after Aho, Weinberger, and Kernighan, the language's original authors.",
|
||||
"sed": "sed stands for 'stream editor', highlighting that it edits text as a flowing stream.",
|
||||
"ps": "ps expands to 'process status'.",
|
||||
"df": "df expands to 'disk free'.",
|
||||
"du": "du expands to 'disk usage'.",
|
||||
"ss": "ss expands to 'socket statistics' and was designed as a faster successor to parts of netstat.",
|
||||
"lsof": "lsof expands to 'list open files'.",
|
||||
"nohup": "nohup literally means 'no hangup' because it ignores the hangup signal when a terminal closes.",
|
||||
"crontab": "crontab means 'cron table', the per-user schedule file for cron jobs.",
|
||||
"apt": "apt is short for 'Advanced Package Tool'.",
|
||||
"yum": "yum originally expanded to 'Yellowdog Updater, Modified'.",
|
||||
"dpkg": "dpkg is the low-level Debian package management tool.",
|
||||
"rpm": "rpm refers to the RPM package manager format and toolchain.",
|
||||
"dig": "dig is short for 'Domain Information Groper', a famously odd but memorable DNS tool name.",
|
||||
"curl": "curl is a client-side URL transfer tool; the name is intentionally short and terminal-friendly.",
|
||||
"wget": "wget historically comes from the idea of 'web get'.",
|
||||
"netstat": "netstat expands to 'network statistics'.",
|
||||
"systemctl": "systemctl is the systemd control command for units such as services, timers, and sockets.",
|
||||
"journalctl": "journalctl is the control/query tool for the systemd journal.",
|
||||
"whoami": "whoami reads like a sentence because it answers the most practical shell identity question directly.",
|
||||
"whereis": "whereis focuses on where a binary, source tree, and manual page live.",
|
||||
"which": "which answers which executable the shell would run first.",
|
||||
"ping": "ping was named after sonar ping sounds; the common acronym explanation is popular but not official.",
|
||||
}
|
||||
|
||||
COMMAND_FUN_FACTS = {
|
||||
"pwd": "Strong operators use pwd almost like a seatbelt: quick, boring, and essential before riskier actions.",
|
||||
"ls": "Many incidents start with a wrong assumption about what exists, which is why ls remains a first-response habit.",
|
||||
"grep": "The command name looks cryptic until you learn its editor history, and then you start seeing Unix culture in miniature.",
|
||||
"awk": "awk is tiny in name but surprisingly powerful; many shell one-liners quietly depend on it for structured text work.",
|
||||
"sed": "sed shines when you need a quick surgical text transform without opening an editor.",
|
||||
"ss": "On modern Linux hosts, ss is often the better first stop than netstat for live socket checks.",
|
||||
"curl": "curl often becomes the bridge between 'the service is up' and 'the application is actually responding correctly'.",
|
||||
"systemctl": "systemctl teaches a crucial operations lesson: a process existing is not the same as a service being healthy.",
|
||||
"journalctl": "journalctl turns logs into a timeline, which is exactly what you need when a deployment fails under time pressure.",
|
||||
"nohup": "nohup is a good reminder that shell sessions and process lifetime are related, but not the same thing.",
|
||||
"dig": "dig is one of the most useful commands for proving that a problem is DNS, or proving that it is not DNS.",
|
||||
"history": "history is not only memory; it is a personal operations notebook showing how you solved similar problems before.",
|
||||
}
|
||||
|
||||
COMMAND_VARIANTS = {
|
||||
"pwd": [
|
||||
{"syntax": "pwd", "purpose": "Print the current logical working directory.", "example": "pwd"},
|
||||
{"syntax": "pwd -P", "purpose": "Resolve symbolic links and show the physical path.", "example": "pwd -P"},
|
||||
],
|
||||
"ls": [
|
||||
{"syntax": "ls -l", "purpose": "Show a long listing with permissions, owner, size, and time.", "example": "ls -l /var/log"},
|
||||
{"syntax": "ls -la", "purpose": "Include hidden entries such as dotfiles.", "example": "ls -la ~"},
|
||||
{"syntax": "ls -lh", "purpose": "Format file sizes in human-readable units.", "example": "ls -lh build"},
|
||||
],
|
||||
"cd": [
|
||||
{"syntax": "cd /path", "purpose": "Jump to a specific directory.", "example": "cd /var/log"},
|
||||
{"syntax": "cd -", "purpose": "Return to the previous directory quickly.", "example": "cd -"},
|
||||
{"syntax": "cd ~", "purpose": "Go back to the current user's home directory.", "example": "cd ~"},
|
||||
],
|
||||
"cat": [
|
||||
{"syntax": "cat file", "purpose": "Print the full contents of a file.", "example": "cat /etc/os-release"},
|
||||
{"syntax": "cat -n file", "purpose": "Show line numbers while printing.", "example": "cat -n app.log"},
|
||||
],
|
||||
"echo": [
|
||||
{"syntax": "echo text", "purpose": "Print a string or quick note.", "example": "echo hello"},
|
||||
{"syntax": "echo $PATH", "purpose": "Inspect shell expansion and environment values.", "example": "echo $PATH"},
|
||||
],
|
||||
"mkdir": [
|
||||
{"syntax": "mkdir dir", "purpose": "Create one directory.", "example": "mkdir logs"},
|
||||
{"syntax": "mkdir -p a/b/c", "purpose": "Create parent directories in one run.", "example": "mkdir -p releases/2026/03"},
|
||||
],
|
||||
"touch": [
|
||||
{"syntax": "touch file", "purpose": "Create an empty file or update its timestamp.", "example": "touch notes.txt"},
|
||||
],
|
||||
"cp": [
|
||||
{"syntax": "cp src dest", "purpose": "Copy one file to another path.", "example": "cp app.conf app.conf.bak"},
|
||||
{"syntax": "cp -r src dest", "purpose": "Copy a directory tree recursively.", "example": "cp -r site site.bak"},
|
||||
{"syntax": "cp -a src dest", "purpose": "Preserve attributes while copying.", "example": "cp -a /etc/nginx nginx.backup"},
|
||||
],
|
||||
"mv": [
|
||||
{"syntax": "mv old new", "purpose": "Rename or move a file or directory.", "example": "mv access.log archive.log"},
|
||||
],
|
||||
"rm": [
|
||||
{"syntax": "rm file", "purpose": "Remove one file.", "example": "rm debug.log"},
|
||||
{"syntax": "rm -r dir", "purpose": "Remove a directory tree recursively.", "example": "rm -r old_build"},
|
||||
{"syntax": "rm -i file", "purpose": "Prompt before deleting.", "example": "rm -i important.txt"},
|
||||
],
|
||||
"chmod": [
|
||||
{"syntax": "chmod 644 file", "purpose": "Set common read/write file permissions.", "example": "chmod 644 app.conf"},
|
||||
{"syntax": "chmod +x script.sh", "purpose": "Add execute permission.", "example": "chmod +x deploy.sh"},
|
||||
],
|
||||
"stat": [
|
||||
{"syntax": "stat file", "purpose": "Inspect inode, timestamps, and file metadata.", "example": "stat access.log"},
|
||||
],
|
||||
"grep": [
|
||||
{"syntax": "grep pattern file", "purpose": "Search matching lines.", "example": "grep ERROR app.log"},
|
||||
{"syntax": "grep -n pattern file", "purpose": "Show line numbers with matches.", "example": "grep -n timeout app.log"},
|
||||
{"syntax": "grep -i pattern file", "purpose": "Ignore letter case.", "example": "grep -i failed auth.log"},
|
||||
],
|
||||
"find": [
|
||||
{"syntax": "find /path -name pattern", "purpose": "Search by file or directory name.", "example": "find /var/log -name '*.log'"},
|
||||
{"syntax": "find /path -type f", "purpose": "Restrict the search to files.", "example": "find . -type f"},
|
||||
{"syntax": "find /path -mtime -1", "purpose": "Filter by recent modification time.", "example": "find /backup -mtime -1"},
|
||||
],
|
||||
"head": [
|
||||
{"syntax": "head file", "purpose": "Show the first 10 lines.", "example": "head app.log"},
|
||||
{"syntax": "head -n 20 file", "purpose": "Show a custom number of leading lines.", "example": "head -n 20 app.log"},
|
||||
],
|
||||
"tail": [
|
||||
{"syntax": "tail file", "purpose": "Show the last 10 lines.", "example": "tail app.log"},
|
||||
{"syntax": "tail -n 50 file", "purpose": "Show more trailing lines.", "example": "tail -n 50 app.log"},
|
||||
{"syntax": "tail -f file", "purpose": "Follow a log while it grows.", "example": "tail -f /var/log/nginx/access.log"},
|
||||
],
|
||||
"sort": [
|
||||
{"syntax": "sort file", "purpose": "Sort text lines.", "example": "sort hosts.txt"},
|
||||
{"syntax": "sort -u file", "purpose": "Sort and deduplicate in one step.", "example": "sort -u users.txt"},
|
||||
],
|
||||
"uniq": [
|
||||
{"syntax": "uniq file", "purpose": "Collapse adjacent duplicates.", "example": "uniq users.txt"},
|
||||
{"syntax": "sort file | uniq -c", "purpose": "Count repeated values after sorting.", "example": "sort status.txt | uniq -c"},
|
||||
],
|
||||
"cut": [
|
||||
{"syntax": "cut -d: -f1 file", "purpose": "Pick fields using a delimiter.", "example": "cut -d: -f1 /etc/passwd"},
|
||||
],
|
||||
"awk": [
|
||||
{"syntax": "awk '{print $1}' file", "purpose": "Print one or more whitespace-separated fields.", "example": "awk '{print $1}' access.log"},
|
||||
{"syntax": "awk -F: '{print $1}' file", "purpose": "Set a custom field separator.", "example": "awk -F: '{print $1}' /etc/passwd"},
|
||||
],
|
||||
"sed": [
|
||||
{"syntax": "sed -n '1,10p' file", "purpose": "Print only selected line ranges.", "example": "sed -n '1,10p' app.log"},
|
||||
{"syntax": "sed 's/old/new/' file", "purpose": "Replace text in the output stream.", "example": "sed 's/http/https/' app.conf"},
|
||||
],
|
||||
"ps": [
|
||||
{"syntax": "ps -ef", "purpose": "Show a full process list.", "example": "ps -ef"},
|
||||
{"syntax": "ps -ef | grep name", "purpose": "Filter for one process family.", "example": "ps -ef | grep nginx"},
|
||||
],
|
||||
"top": [
|
||||
{"syntax": "top", "purpose": "Open an interactive live process view.", "example": "top"},
|
||||
{"syntax": "top -b -n 1", "purpose": "Capture one batch snapshot for logs or pipes.", "example": "top -b -n 1 | head -20"},
|
||||
],
|
||||
"ip": [
|
||||
{"syntax": "ip addr", "purpose": "Show interfaces and assigned IP addresses.", "example": "ip addr"},
|
||||
{"syntax": "ip route", "purpose": "Inspect the routing table.", "example": "ip route"},
|
||||
],
|
||||
"ping": [
|
||||
{"syntax": "ping host", "purpose": "Test reachability continuously.", "example": "ping 8.8.8.8"},
|
||||
{"syntax": "ping -c 4 host", "purpose": "Send a fixed number of probes.", "example": "ping -c 4 example.com"},
|
||||
],
|
||||
"ss": [
|
||||
{"syntax": "ss -lntp", "purpose": "Show listening TCP ports and owning processes.", "example": "ss -lntp"},
|
||||
{"syntax": "ss -s", "purpose": "Print a compact socket summary.", "example": "ss -s"},
|
||||
],
|
||||
"curl": [
|
||||
{"syntax": "curl URL", "purpose": "Fetch a URL quickly from the terminal.", "example": "curl http://localhost:8080/health"},
|
||||
{"syntax": "curl -I URL", "purpose": "Request response headers only.", "example": "curl -I https://example.com"},
|
||||
{"syntax": "curl -X POST URL", "purpose": "Send a request with an explicit HTTP method.", "example": "curl -X POST http://localhost:8080/api/login"},
|
||||
],
|
||||
"wget": [
|
||||
{"syntax": "wget URL", "purpose": "Download a file to disk.", "example": "wget https://example.com/file.tar.gz"},
|
||||
{"syntax": "wget -O name URL", "purpose": "Choose the output filename.", "example": "wget -O app.tar.gz https://example.com/latest.tar.gz"},
|
||||
],
|
||||
"systemctl": [
|
||||
{"syntax": "systemctl status name", "purpose": "Inspect service state and recent status text.", "example": "systemctl status nginx"},
|
||||
{"syntax": "systemctl restart name", "purpose": "Restart a service after config or code changes.", "example": "systemctl restart nginx"},
|
||||
{"syntax": "systemctl enable name", "purpose": "Enable a unit at boot time.", "example": "systemctl enable nginx"},
|
||||
],
|
||||
"journalctl": [
|
||||
{"syntax": "journalctl -u name", "purpose": "Read logs for one systemd unit.", "example": "journalctl -u nginx"},
|
||||
{"syntax": "journalctl -xe", "purpose": "Show recent errors with context.", "example": "journalctl -xe"},
|
||||
{"syntax": "journalctl -u name -n 50", "purpose": "Read the latest lines for one unit.", "example": "journalctl -u nginx -n 50"},
|
||||
],
|
||||
"df": [
|
||||
{"syntax": "df -h", "purpose": "Show filesystem usage in human units.", "example": "df -h"},
|
||||
{"syntax": "df -i", "purpose": "Check inode exhaustion, not just bytes.", "example": "df -i"},
|
||||
],
|
||||
"du": [
|
||||
{"syntax": "du -sh path", "purpose": "Summarize directory size.", "example": "du -sh /var/log"},
|
||||
{"syntax": "du -h path", "purpose": "Break size down recursively.", "example": "du -h /var/log | sort -h | tail"},
|
||||
],
|
||||
"whoami": [
|
||||
{"syntax": "whoami", "purpose": "Print the current account name.", "example": "whoami"},
|
||||
],
|
||||
"id": [
|
||||
{"syntax": "id", "purpose": "Show UID, GID, and groups.", "example": "id"},
|
||||
{"syntax": "id user", "purpose": "Inspect another account if the environment allows it.", "example": "id nginx"},
|
||||
],
|
||||
"env": [
|
||||
{"syntax": "env", "purpose": "Print environment variables.", "example": "env"},
|
||||
{"syntax": "env | grep NAME", "purpose": "Filter the environment for one variable.", "example": "env | grep PATH"},
|
||||
],
|
||||
"export": [
|
||||
{"syntax": "export VAR=value", "purpose": "Set an environment variable for child processes.", "example": "export APP_ENV=prod"},
|
||||
{"syntax": "export PATH=$PATH:/opt/bin", "purpose": "Extend PATH temporarily in the current shell.", "example": "export PATH=$PATH:/opt/bin"},
|
||||
],
|
||||
"alias": [
|
||||
{"syntax": "alias", "purpose": "List current shell aliases.", "example": "alias"},
|
||||
{"syntax": "alias ll='ls -alF'", "purpose": "Create a shortcut for a repeated command.", "example": "alias ll='ls -alF'"},
|
||||
],
|
||||
"which": [
|
||||
{"syntax": "which cmd", "purpose": "Show the executable the shell would run first.", "example": "which python"},
|
||||
],
|
||||
"whereis": [
|
||||
{"syntax": "whereis cmd", "purpose": "Locate binary, source, and manual page paths.", "example": "whereis nginx"},
|
||||
],
|
||||
"apt": [
|
||||
{"syntax": "apt search name", "purpose": "Search repositories for packages.", "example": "apt search nginx"},
|
||||
{"syntax": "apt install name", "purpose": "Install a package.", "example": "apt install nginx"},
|
||||
{"syntax": "apt remove name", "purpose": "Remove a package.", "example": "apt remove nginx"},
|
||||
],
|
||||
"yum": [
|
||||
{"syntax": "yum search name", "purpose": "Search repositories.", "example": "yum search nginx"},
|
||||
{"syntax": "yum install name", "purpose": "Install a package.", "example": "yum install nginx"},
|
||||
],
|
||||
"rpm": [
|
||||
{"syntax": "rpm -qa", "purpose": "List installed RPM packages.", "example": "rpm -qa | grep nginx"},
|
||||
{"syntax": "rpm -qi name", "purpose": "Inspect one package in detail.", "example": "rpm -qi nginx"},
|
||||
],
|
||||
"dpkg": [
|
||||
{"syntax": "dpkg -l", "purpose": "List installed Debian packages.", "example": "dpkg -l | grep nginx"},
|
||||
{"syntax": "dpkg -L name", "purpose": "List files installed by one package.", "example": "dpkg -L nginx"},
|
||||
],
|
||||
"tar": [
|
||||
{"syntax": "tar -tf file.tar", "purpose": "List archive contents without extracting.", "example": "tar -tf backup.tar"},
|
||||
{"syntax": "tar -czf out.tar.gz dir", "purpose": "Create a gzip-compressed archive.", "example": "tar -czf logs.tar.gz /var/log/myapp"},
|
||||
{"syntax": "tar -xzf file.tar.gz", "purpose": "Extract a gzip-compressed archive.", "example": "tar -xzf logs.tar.gz"},
|
||||
],
|
||||
"nohup": [
|
||||
{"syntax": "nohup cmd &", "purpose": "Keep a process alive after terminal disconnect.", "example": "nohup python3 server.py &"},
|
||||
],
|
||||
"uptime": [
|
||||
{"syntax": "uptime", "purpose": "Show uptime and load averages.", "example": "uptime"},
|
||||
],
|
||||
"free": [
|
||||
{"syntax": "free -h", "purpose": "Show memory usage in human units.", "example": "free -h"},
|
||||
],
|
||||
"lsof": [
|
||||
{"syntax": "lsof -i :port", "purpose": "Find the process using a port.", "example": "lsof -i :8080"},
|
||||
{"syntax": "lsof file", "purpose": "Find which process has a file open.", "example": "lsof /var/log/app.log"},
|
||||
],
|
||||
"netstat": [
|
||||
{"syntax": "netstat -lntp", "purpose": "Legacy view of listening TCP ports.", "example": "netstat -lntp"},
|
||||
],
|
||||
"traceroute": [
|
||||
{"syntax": "traceroute host", "purpose": "Trace network hops to a target host.", "example": "traceroute example.com"},
|
||||
],
|
||||
"dig": [
|
||||
{"syntax": "dig name", "purpose": "Query DNS records with full output.", "example": "dig example.com"},
|
||||
{"syntax": "dig name +short", "purpose": "Print compact answers only.", "example": "dig example.com +short"},
|
||||
],
|
||||
"date": [
|
||||
{"syntax": "date", "purpose": "Print the current system time.", "example": "date"},
|
||||
{"syntax": "date '+%F %T'", "purpose": "Format the output explicitly.", "example": "date '+%F %T'"},
|
||||
],
|
||||
"cal": [
|
||||
{"syntax": "cal", "purpose": "Show the current month calendar.", "example": "cal"},
|
||||
{"syntax": "cal 2026", "purpose": "Show a whole year calendar.", "example": "cal 2026"},
|
||||
],
|
||||
"crontab": [
|
||||
{"syntax": "crontab -l", "purpose": "List current cron jobs.", "example": "crontab -l"},
|
||||
{"syntax": "crontab -e", "purpose": "Edit the cron table.", "example": "crontab -e"},
|
||||
],
|
||||
"history": [
|
||||
{"syntax": "history", "purpose": "Show previous shell commands.", "example": "history"},
|
||||
{"syntax": "history | grep word", "purpose": "Search for a prior command pattern.", "example": "history | grep tar"},
|
||||
],
|
||||
"last": [
|
||||
{"syntax": "last", "purpose": "Show recent login records.", "example": "last"},
|
||||
],
|
||||
"w": [
|
||||
{"syntax": "w", "purpose": "Show who is logged in and what they are doing.", "example": "w"},
|
||||
],
|
||||
"clear": [
|
||||
{"syntax": "clear", "purpose": "Clear terminal output without changing shell state.", "example": "clear"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def command_category(command: str) -> str:
|
||||
normalized = command.strip().lower()
|
||||
for category, commands in COMMAND_CATEGORIES.items():
|
||||
if normalized in commands:
|
||||
return category
|
||||
return "general"
|
||||
|
||||
|
||||
def command_origin(command: str) -> str:
|
||||
normalized = command.strip().lower()
|
||||
return COMMAND_ORIGINS.get(
|
||||
normalized,
|
||||
f"{normalized} follows the classic Unix habit of using short names so repeated terminal work stays fast.",
|
||||
)
|
||||
|
||||
|
||||
def command_fun_fact(command: str) -> str:
|
||||
normalized = command.strip().lower()
|
||||
return COMMAND_FUN_FACTS.get(
|
||||
normalized,
|
||||
f"Treat {normalized} as part of a command chain: its real value appears when it informs the next observation or repair step.",
|
||||
)
|
||||
|
||||
|
||||
def generic_variant(command: str) -> list[dict[str, str]]:
|
||||
normalized = command.strip().lower()
|
||||
if normalized in {"cd", "export", "alias"}:
|
||||
return [
|
||||
{
|
||||
"syntax": f"help {normalized}",
|
||||
"purpose": "Inspect builtin shell help before using unfamiliar forms.",
|
||||
"example": f"help {normalized}",
|
||||
}
|
||||
]
|
||||
return [
|
||||
{
|
||||
"syntax": f"{normalized} --help",
|
||||
"purpose": "Review the supported options before using the command in a new situation.",
|
||||
"example": f"{normalized} --help",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def command_variants(command: str) -> list[dict[str, str]]:
|
||||
normalized = command.strip().lower()
|
||||
return COMMAND_VARIANTS.get(normalized, generic_variant(normalized))
|
||||
|
||||
|
||||
def build_command_guides(commands: list[str]) -> list[dict[str, Any]]:
|
||||
guides: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for command in commands:
|
||||
normalized = command.strip().lower()
|
||||
if not normalized or normalized in seen:
|
||||
continue
|
||||
seen.add(normalized)
|
||||
guides.append(
|
||||
{
|
||||
"command": normalized,
|
||||
"role": CATEGORY_ROLES.get(command_category(normalized), CATEGORY_ROLES["general"]),
|
||||
"origin": command_origin(normalized),
|
||||
"fun_fact": command_fun_fact(normalized),
|
||||
"variants": command_variants(normalized),
|
||||
}
|
||||
)
|
||||
return guides
|
||||
|
||||
|
||||
def build_ops_extensions(commands: list[str]) -> list[str]:
|
||||
items: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for command in commands:
|
||||
category = command_category(command)
|
||||
for tip in CATEGORY_EXTENSION_TIPS.get(category, []):
|
||||
if tip not in seen:
|
||||
items.append(tip)
|
||||
seen.add(tip)
|
||||
closing_notes = [
|
||||
"Run the simplest read-only form first, then explain what stronger or riskier form you would use only if the evidence demands it.",
|
||||
"Write a tiny playbook note for this lesson: goal -> command -> key output -> next step.",
|
||||
]
|
||||
for note in closing_notes:
|
||||
if note not in seen:
|
||||
items.append(note)
|
||||
seen.add(note)
|
||||
return items[:6]
|
||||
|
||||
|
||||
def build_review_prompts(commands: list[str], lesson_goal: str) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
f"What system question are you trying to answer with {label} before you touch the system state?",
|
||||
f"Which output line, field, or signal matters most for this goal: {lesson_goal}",
|
||||
f"What narrower or safer syntax variant would you try first if the default output feels too broad or too risky?",
|
||||
f"After {label}, what should the next verification, comparison, or repair step be in a real operations flow?",
|
||||
]
|
||||
|
||||
|
||||
def flatten_lessons() -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for module in COURSE.get("modules", []):
|
||||
@@ -295,7 +741,7 @@ def build_diagnostics() -> dict[str, Any]:
|
||||
solution_commands = sorted(set(extract_solution_commands()))
|
||||
required_commands = sorted(set(course_commands) | set(solution_commands))
|
||||
supported_commands = sorted(SANDBOX.supported_commands())
|
||||
unsupported_commands = [command for command in required_commands if command not in SANDBOX.supported_commands()]
|
||||
unsupported_commands = [command for command in required_commands if not is_supported_command_token(command)]
|
||||
total_exercises = sum(len(lesson.get("exercises", [])) for lesson in lessons)
|
||||
operation_exercises = sum(
|
||||
1
|
||||
@@ -309,7 +755,7 @@ def build_diagnostics() -> dict[str, Any]:
|
||||
"supported_command_count": len(supported_commands),
|
||||
"required_command_count": len(required_commands),
|
||||
"covered_command_count": len(required_commands) - len(unsupported_commands),
|
||||
"coverage_rate": 100 if not required_commands else round(((len(required_commands) - len(unsupported_commands)) / len(required_commands)) * 100)),
|
||||
"coverage_rate": 100 if not required_commands else round(((len(required_commands) - len(unsupported_commands)) / len(required_commands)) * 100),
|
||||
"unsupported_commands": unsupported_commands,
|
||||
"course_commands": required_commands,
|
||||
"operation_exercises": operation_exercises,
|
||||
@@ -354,6 +800,7 @@ def build_lesson_payload(module: dict[str, Any], lesson: dict[str, Any]) -> dict
|
||||
lesson_ids = [item.get("id") for item in all_lessons]
|
||||
current_id = lesson.get("id")
|
||||
index = lesson_ids.index(current_id) if current_id in lesson_ids else -1
|
||||
goal = safe_text(lesson.get("goal"), default_goal(commands))
|
||||
|
||||
previous_lesson = None
|
||||
next_lesson = None
|
||||
@@ -369,7 +816,7 @@ def build_lesson_payload(module: dict[str, Any], lesson: dict[str, Any]) -> dict
|
||||
return {
|
||||
"id": lesson.get("id"),
|
||||
"display_title": lesson_title(lesson),
|
||||
"display_goal": safe_text(lesson.get("goal"), default_goal(commands)),
|
||||
"display_goal": goal,
|
||||
"display_why": safe_text(lesson.get("why_it_matters"), default_why(commands)),
|
||||
"display_module_title": module_title(module),
|
||||
"display_module_summary": module_summary(module),
|
||||
@@ -387,6 +834,9 @@ def build_lesson_payload(module: dict[str, Any], lesson: dict[str, Any]) -> dict
|
||||
),
|
||||
"after_class": safe_text(lesson.get("after_class"), default_after_class(commands)),
|
||||
"related_commands": safe_list(lesson.get("related_commands"), commands or ["pwd"]),
|
||||
"command_guides": build_command_guides(commands),
|
||||
"ops_extensions": build_ops_extensions(commands),
|
||||
"review_prompts": build_review_prompts(commands, goal),
|
||||
"exercises": [build_exercise(exercise, lesson) for exercise in lesson.get("exercises", [])],
|
||||
"exercise_count": len(lesson.get("exercises", [])),
|
||||
"module_id": module.get("id"),
|
||||
|
||||
Reference in New Issue
Block a user