@@ -32,26 +32,40 @@ def main():
3232
3333 # Check command
3434 check_parser = subparsers .add_parser ("check" , help = "Check your code against tests" )
35- check_parser .add_argument ("slug" , help = "The check slug (e.g., course- cs50/mario-less)" )
35+ check_parser .add_argument ("slug" , help = "The check slug (e.g., cs50/mario-less)" )
3636 check_parser .add_argument ("-o" , "--output" , choices = ["ansi" , "json" ], default = "ansi" ,
3737 help = "Output format (default: ansi)" )
3838 check_parser .add_argument ("--log" , action = "store_true" , help = "Show detailed log" )
3939 check_parser .add_argument ("--target" , action = "append" , metavar = "check" ,
4040 help = "Run only the specified check(s)" )
41+ check_parser .add_argument ("-L" , "--language" , default = "c" ,
42+ help = "Language for checks (default: c)" )
43+ check_parser .add_argument ("-u" , "--update" , action = "store_true" ,
44+ help = "Force update checks from remote" )
4145 check_parser .add_argument ("--local" , metavar = "PATH" , help = "Path to local checks directory" )
4246
4347 # Submit command
4448 submit_parser = subparsers .add_parser ("submit" , help = "Submit your code" )
45- submit_parser .add_argument ("slug" , help = "The submission slug (e.g., course- cs50/hello)" )
49+ submit_parser .add_argument ("slug" , help = "The submission slug (e.g., cs50/hello)" )
4650 submit_parser .add_argument ("-m" , "--message" , help = "Commit message" )
4751 submit_parser .add_argument ("-y" , "--yes" , action = "store_true" , help = "Skip confirmation" )
52+ submit_parser .add_argument ("-L" , "--language" , help = "Language of submission (auto-detected if not specified)" )
4853 submit_parser .add_argument ("--local" , metavar = "PATH" , help = "Path to local checks directory (for file list)" )
4954
5055 # Auth commands
5156 subparsers .add_parser ("login" , help = "Log in with GitHub" )
5257 subparsers .add_parser ("logout" , help = "Log out" )
5358 subparsers .add_parser ("whoami" , help = "Show logged in user" )
5459
60+ # Cache command
61+ cache_parser = subparsers .add_parser ("cache" , help = "Manage checks cache" )
62+ cache_parser .add_argument ("action" , choices = ["clear" , "list" ],
63+ help = "Action to perform" )
64+ cache_parser .add_argument ("slug" , nargs = "?" ,
65+ help = "Specific course or course/stage slug (optional)" )
66+ cache_parser .add_argument ("-L" , "--language" ,
67+ help = "Specific language (optional)" )
68+
5569 args = parser .parse_args ()
5670
5771 if args .command == "check" :
@@ -64,6 +78,8 @@ def main():
6478 return run_logout (args )
6579 elif args .command == "whoami" :
6680 return run_whoami (args )
81+ elif args .command == "cache" :
82+ return run_cache (args )
6783 else :
6884 parser .print_help ()
6985 return 1
@@ -72,17 +88,19 @@ def main():
7288def run_check (args ):
7389 """Run the check command."""
7490 slug = args .slug
91+ language = getattr (args , 'language' , 'c' ) or 'c'
92+ force_update = getattr (args , 'update' , False )
7593
7694 # Determine check directory
7795 if args .local :
7896 check_dir = Path (args .local ).resolve ()
7997 else :
80- # For now, look for checks in a local directory structure
81- # Future: download from remote like check50 does
82- check_dir = find_check_dir (slug )
98+ # Try remote download first, then fall back to local search
99+ check_dir = find_check_dir (slug , language = language , force_update = force_update )
83100
84101 if not check_dir or not check_dir .exists ():
85102 termcolor .cprint (f"Error: Could not find checks for '{ slug } '" , "red" , file = sys .stderr )
103+ termcolor .cprint ("Use --local to specify a local checks directory." , "yellow" , file = sys .stderr )
86104 return 1
87105
88106 # Set internal state
@@ -220,12 +238,50 @@ def output_json(results, show_log=False):
220238 print (json .dumps (output , indent = 2 ))
221239
222240
223- def find_check_dir (slug ):
224- """Find the check directory for a given slug."""
225- # Extract stage name from slug (e.g., "cs50/credit" -> "credit")
226- stage_name = slug .split ("/" )[- 1 ] if "/" in slug else slug
241+ def find_check_dir (slug , language : str = "c" , force_update : bool = False ):
242+ """
243+ Find the check directory for a given slug.
244+
245+ Priority:
246+ 1. BOOTCS_CHECKS_PATH environment variable (for evaluator)
247+ 2. Remote API download (with local cache)
248+ 3. Local directories (for development)
249+ """
250+ # Extract parts from slug (e.g., "cs50/credit" -> course="cs50", stage="credit")
251+ parts = slug .split ("/" )
252+ if len (parts ) == 2 :
253+ course_slug , stage_name = parts
254+ else :
255+ stage_name = slug
256+ course_slug = None
227257
228- # Common locations to search
258+ # 1. Check environment variable first (used by evaluator)
259+ if "BOOTCS_CHECKS_PATH" in os .environ :
260+ checks_path = Path (os .environ ["BOOTCS_CHECKS_PATH" ])
261+ # Try with stage name directly
262+ path = checks_path / stage_name
263+ if path .exists ():
264+ return path
265+ # Try with full slug
266+ if course_slug :
267+ path = checks_path / slug
268+ if path .exists ():
269+ return path
270+
271+ # 2. Try remote download (if slug has course/stage format)
272+ if course_slug and "/" in slug :
273+ try :
274+ from .api .checks import get_checks_manager
275+ manager = get_checks_manager ()
276+ check_path = manager .get_checks (slug , language = language , force_update = force_update )
277+ if check_path .exists ():
278+ return check_path
279+ except Exception as e :
280+ # Log error but continue to local fallback
281+ import sys
282+ print (f"Warning: Could not download checks: { e } " , file = sys .stderr )
283+
284+ # 3. Local directories fallback (for development)
229285 search_paths = [
230286 Path .cwd () / "checks" / slug ,
231287 Path .cwd () / "checks" / stage_name ,
@@ -234,12 +290,6 @@ def find_check_dir(slug):
234290 Path .home () / ".local" / "share" / "bootcs" / slug ,
235291 ]
236292
237- # Also check environment variable (try both full slug and stage name)
238- if "BOOTCS_CHECKS_PATH" in os .environ :
239- checks_path = Path (os .environ ["BOOTCS_CHECKS_PATH" ])
240- search_paths .insert (0 , checks_path / stage_name )
241- search_paths .insert (0 , checks_path / slug )
242-
243293 for path in search_paths :
244294 if path .exists ():
245295 return path
@@ -254,6 +304,7 @@ def run_submit(args):
254304 from .api .submit import collect_files , submit_files , SubmitFile
255305
256306 slug = args .slug
307+ language = getattr (args , 'language' , None ) # Optional, will be auto-detected if not provided
257308
258309 # Check if logged in
259310 if not is_logged_in ():
@@ -264,10 +315,12 @@ def run_submit(args):
264315 token = get_token ()
265316
266317 # Determine check directory for file list
318+ # Use provided language or default to 'c' for finding checks
319+ check_language = language or 'c'
267320 if args .local :
268321 check_dir = Path (args .local ).resolve ()
269322 else :
270- check_dir = find_check_dir (slug )
323+ check_dir = find_check_dir (slug , language = check_language )
271324
272325 if not check_dir or not check_dir .exists ():
273326 termcolor .cprint (f"Error: Could not find config for '{ slug } '" , "red" , file = sys .stderr )
@@ -331,6 +384,7 @@ def run_submit(args):
331384 files = files ,
332385 token = token ,
333386 message = args .message ,
387+ language = language , # Pass language if specified
334388 )
335389 print ()
336390 print ()
@@ -469,5 +523,56 @@ def run_whoami(args):
469523 return 0
470524
471525
526+ def run_cache (args ):
527+ """Run the cache command."""
528+ from .api .checks import get_checks_manager
529+
530+ action = args .action
531+ slug = getattr (args , 'slug' , None )
532+ language = getattr (args , 'language' , None )
533+
534+ manager = get_checks_manager ()
535+
536+ if action == "list" :
537+ # List cached checks
538+ cached = manager .list_cache ()
539+
540+ if not cached :
541+ termcolor .cprint ("No cached checks." , "yellow" )
542+ return 0
543+
544+ print ()
545+ termcolor .cprint ("📦 Cached Checks:" , "cyan" , attrs = ["bold" ])
546+ print ()
547+ print (f" { 'Course' :<10} { 'Language' :<10} { 'Stage' :<15} { 'Version' :<10} { 'Age' :<6} " )
548+ print (f" { '-' * 10 } { '-' * 10 } { '-' * 15 } { '-' * 10 } { '-' * 6 } " )
549+
550+ for item in cached :
551+ print (f" { item ['course' ]:<10} { item ['language' ]:<10} { item ['stage' ]:<15} { item ['version' ]:<10} { item ['age' ]:<6} " )
552+
553+ print ()
554+ print (f" Total: { len (cached )} cached checks" )
555+ print (f" Location: { manager .cache_dir } " )
556+ print ()
557+ return 0
558+
559+ elif action == "clear" :
560+ # Clear cache
561+ try :
562+ manager .clear_cache (slug = slug , language = language )
563+
564+ if slug :
565+ termcolor .cprint (f"✅ Cleared cache for '{ slug } '" , "green" )
566+ else :
567+ termcolor .cprint ("✅ Cleared all cached checks" , "green" )
568+
569+ return 0
570+ except ValueError as e :
571+ termcolor .cprint (f"❌ Error: { e } " , "red" )
572+ return 1
573+
574+ return 0
575+
576+
472577if __name__ == "__main__" :
473578 sys .exit (main ())
0 commit comments