From 65d119cdfcc5c333b67c87b5ff36c153accf6fa8 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:14:18 -0500 Subject: [PATCH 1/9] fix: remove bag files --- t/minimal_init.lua | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 t/minimal_init.lua diff --git a/t/minimal_init.lua b/t/minimal_init.lua deleted file mode 100644 index 1f56d27..0000000 --- a/t/minimal_init.lua +++ /dev/null @@ -1,21 +0,0 @@ -vim.opt.runtimepath:prepend(vim.fn.expand('~/dev/cp.nvim')) -vim.opt.runtimepath:prepend(vim.fn.expand('~/dev/fzf-lua')) - -vim.g.cp = { - languages = { - cpp = { - extension = 'cc', - commands = { - build = { 'g++', '-std=c++23', '-O2', '{source}', '-o', '{binary}' }, - run = { '{binary}' }, - }, - }, - }, - platforms = { - codechef = { - enabled_languages = { 'cpp' }, - default_language = 'cpp', - }, - }, - ui = { picker = 'fzf-lua' }, -} From 397576ad935d8e207b5a9b0dd419005e67a38d2a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:14:54 -0500 Subject: [PATCH 2/9] fix(cses): add token fast path to login and improve error surfacing Problem: `login()` always ran the full web login flow even with a valid cached token, prompting the user unnecessarily. Submit errors only checked `message`, missing `error` field. Solution: check the cached token via `_check_token` at the start of `login()`; return immediately if valid. Error body now checks `body.get("error") or body.get("message")` before falling back to raw text. --- scrapers/cses.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scrapers/cses.py b/scrapers/cses.py index 2d29689..4f1fbcf 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -248,7 +248,21 @@ class CSESScraper(BaseScraper): if not username or not password: return self._login_error("Missing username or password") + token = credentials.get("token") async with httpx.AsyncClient(follow_redirects=True) as client: + if token: + print(json.dumps({"status": "checking_login"}), flush=True) + if await self._check_token(client, token): + return LoginResult( + success=True, + error="", + credentials={ + "username": username, + "password": password, + "token": token, + }, + ) + print(json.dumps({"status": "logging_in"}), flush=True) token = await self._web_login(client, username, password) if not token: @@ -460,7 +474,8 @@ class CSESScraper(BaseScraper): if r.status_code not in range(200, 300): try: - err = r.json().get("message", r.text) + body = r.json() + err = body.get("error") or body.get("message") or r.text except Exception: err = r.text return self._submit_error(f"Submit request failed: {err}") From 923dc7293fa2c8d091e20949289e43a8f876c650 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:15:42 -0500 Subject: [PATCH 3/9] fix(kattis): harden reactive re-auth trigger on submit Problem: stale cookie detection only matched the exact text `"Request validation failed"`, missing cases where Kattis returns a 400 or 403 status instead. Solution: also trigger re-login when `r.status_code in (400, 403)`. --- scrapers/kattis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapers/kattis.py b/scrapers/kattis.py index 3ace5ba..fc80ff5 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -366,7 +366,7 @@ class KattisScraper(BaseScraper): except Exception as e: return self._submit_error(f"Submit request failed: {e}") - if r.text == "Request validation failed": + if r.status_code in (400, 403) or r.text == "Request validation failed": _COOKIE_PATH.unlink(missing_ok=True) print(json.dumps({"status": "logging_in"}), flush=True) ok = await _do_kattis_login(client, username, password) From a47decf81b1fc8fcfd89918fe90fcfa4674aff55 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:19:54 -0500 Subject: [PATCH 4/9] fix(kattis): emit checking_login at submit start for consistency --- scrapers/kattis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scrapers/kattis.py b/scrapers/kattis.py index fc80ff5..1177445 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -329,6 +329,7 @@ class KattisScraper(BaseScraper): return self._submit_error("Missing credentials. Use :CP kattis login") async with httpx.AsyncClient(follow_redirects=True) as client: + print(json.dumps({"status": "checking_login"}), flush=True) await _load_kattis_cookies(client) if not client.cookies: print(json.dumps({"status": "logging_in"}), flush=True) From ffd40564bcafb6a8c99e1210ce8db37dd905b215 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:20:20 -0500 Subject: [PATCH 5/9] fix(usaco): add proactive cookie validation to login and submit Problem: `login()` always ran a fresh web login even with valid cached cookies. `submit()` only checked cookie existence, not validity, relying solely on a reactive retry after auth failure. Auth redirect detection used `page_r.url.path` which could miss non-path login redirects. Solution: `login()` and `submit()` now load cookies and call `_check_usaco_login()` upfront; re-login only if the check fails. Auth detection in `_do_submit()` uses `str(page_r.url)` for a more robust redirect match. --- scrapers/usaco.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/scrapers/usaco.py b/scrapers/usaco.py index b3970db..b009cf0 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -429,7 +429,19 @@ class USACOScraper(BaseScraper): async with httpx.AsyncClient(follow_redirects=True) as client: await _load_usaco_cookies(client) - if not client.cookies: + if client.cookies: + print(json.dumps({"status": "checking_login"}), flush=True) + if not await _check_usaco_login(client, username): + client.cookies.clear() + print(json.dumps({"status": "logging_in"}), flush=True) + try: + ok = await _do_usaco_login(client, username, password) + except Exception as e: + return self._submit_error(f"Login failed: {e}") + if not ok: + return self._submit_error("Login failed (bad credentials?)") + await _save_usaco_cookies(client) + else: print(json.dumps({"status": "logging_in"}), flush=True) try: ok = await _do_usaco_login(client, username, password) @@ -470,7 +482,8 @@ class USACOScraper(BaseScraper): headers=HEADERS, timeout=HTTP_TIMEOUT, ) - if "login" in page_r.url.path.lower() or "Login" in page_r.text[:2000]: + page_url = str(page_r.url) + if "/login" in page_url or "Login" in page_r.text[:2000]: return self._submit_error("auth_failure") form_url, hidden_fields, lang_val = _parse_submit_form( page_r.text, language_id @@ -513,6 +526,16 @@ class USACOScraper(BaseScraper): return self._login_error("Missing username or password") async with httpx.AsyncClient(follow_redirects=True) as client: + await _load_usaco_cookies(client) + if client.cookies: + print(json.dumps({"status": "checking_login"}), flush=True) + if await _check_usaco_login(client, username): + return LoginResult( + success=True, + error="", + credentials={"username": username, "password": password}, + ) + print(json.dumps({"status": "logging_in"}), flush=True) try: ok = await _do_usaco_login(client, username, password) From e6441ad63044b50344520a862d7c271e6f462662 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:20:54 -0500 Subject: [PATCH 6/9] fix(credentials): try cached credentials before prompting on login Problem: `:CP login` always prompted for username and password, even when valid credentials were already cached. Solution: if cached credentials exist, attempt `scraper.login()` with them first. On success, return immediately with no prompt. On failure, fall through to `prompt_and_login()` for fresh input. --- lua/cp/credentials.lua | 77 ++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index 5328b8a..835199f 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -11,6 +11,43 @@ local STATUS_MESSAGES = { installing_browser = 'Installing browser...', } +---@param platform string +---@param display string +local function prompt_and_login(platform, display) + vim.ui.input({ prompt = display .. ' username: ' }, function(username) + if not username or username == '' then + logger.log('Cancelled', { level = vim.log.levels.WARN }) + return + end + vim.fn.inputsave() + local password = vim.fn.inputsecret(display .. ' password: ') + vim.fn.inputrestore() + if not password or password == '' then + logger.log('Cancelled', { level = vim.log.levels.WARN }) + return + end + + local credentials = { username = username, password = password } + + local scraper = require('cp.scraper') + scraper.login(platform, credentials, function(ev) + vim.schedule(function() + local msg = STATUS_MESSAGES[ev.status] or ev.status + logger.log(display .. ': ' .. msg, { level = vim.log.levels.INFO, override = true }) + end) + end, function(result) + vim.schedule(function() + if result.success then + logger.log(display .. ' login successful', { level = vim.log.levels.INFO, override = true }) + else + local err = result.error or 'unknown error' + logger.log(display .. ' login failed: ' .. err, { level = vim.log.levels.ERROR }) + end + end) + end) + end) +end + ---@param platform string? function M.login(platform) platform = platform or state.get_platform() @@ -24,31 +61,12 @@ function M.login(platform) local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform - vim.ui.input({ prompt = display .. ' username: ' }, function(username) - if not username or username == '' then - logger.log('Cancelled', { level = vim.log.levels.WARN }) - return - end - vim.fn.inputsave() - local password = vim.fn.inputsecret(display .. ' password: ') - vim.fn.inputrestore() - if not password or password == '' then - logger.log('Cancelled', { level = vim.log.levels.WARN }) - return - end - - cache.load() - local existing = cache.get_credentials(platform) or {} - local credentials = { - username = username, - password = password, - } - if existing.token then - credentials.token = existing.token - end + cache.load() + local existing = cache.get_credentials(platform) or {} + if existing.username and existing.password then local scraper = require('cp.scraper') - scraper.login(platform, credentials, function(ev) + scraper.login(platform, existing, function(ev) vim.schedule(function() local msg = STATUS_MESSAGES[ev.status] or ev.status logger.log(display .. ': ' .. msg, { level = vim.log.levels.INFO, override = true }) @@ -56,17 +74,16 @@ function M.login(platform) end, function(result) vim.schedule(function() if result.success then - logger.log( - display .. ' login successful', - { level = vim.log.levels.INFO, override = true } - ) + logger.log(display .. ' login successful', { level = vim.log.levels.INFO, override = true }) else - local err = result.error or 'unknown error' - logger.log(display .. ' login failed: ' .. err, { level = vim.log.levels.ERROR }) + prompt_and_login(platform, display) end end) end) - end) + return + end + + prompt_and_login(platform, display) end ---@param platform string? From 3c86af869f8cbf891f62f8fe3c8320a58ed9d58d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:21:00 -0500 Subject: [PATCH 7/9] fix(submit): remove premature Submitting log before scraper starts --- lua/cp/submit.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index e7146fe..6418488 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -79,7 +79,6 @@ function M.submit(opts) prompt_credentials(platform, function(creds) vim.cmd.update() - logger.log('Submitting...', { level = vim.log.levels.INFO, override = true }) require('cp.scraper').submit( platform, From cf175a80c1945cb65e6905b02d90a6bb83d34442 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:23:28 -0500 Subject: [PATCH 8/9] ci: format --- lua/cp/credentials.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index 835199f..637ed23 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -38,7 +38,10 @@ local function prompt_and_login(platform, display) end, function(result) vim.schedule(function() if result.success then - logger.log(display .. ' login successful', { level = vim.log.levels.INFO, override = true }) + logger.log( + display .. ' login successful', + { level = vim.log.levels.INFO, override = true } + ) else local err = result.error or 'unknown error' logger.log(display .. ' login failed: ' .. err, { level = vim.log.levels.ERROR }) @@ -74,7 +77,10 @@ function M.login(platform) end, function(result) vim.schedule(function() if result.success then - logger.log(display .. ' login successful', { level = vim.log.levels.INFO, override = true }) + logger.log( + display .. ' login successful', + { level = vim.log.levels.INFO, override = true } + ) else prompt_and_login(platform, display) end From 4f307e323d0b4efea1edc7c6c3b43393a00bab8d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 02:32:52 -0500 Subject: [PATCH 9/9] fix(config): avoid doubling slug in `default_filename` for single-problem mode Problem: Kattis single-problem mode sets both `contest_id` and `problem_id` to the same slug, causing `default_filename` to concatenate them (e.g. `addtwonumbersaddtwonumbers.cc`). Solution: only concatenate when `problem_id ~= contest_id`; otherwise return just `problem_id` (or `contest_id` if nil). --- lua/cp/config.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 74a40b7..2291101 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -605,10 +605,10 @@ end ---@param problem_id? string ---@return string local function default_filename(contest_id, problem_id) - if problem_id then + if problem_id and problem_id ~= contest_id then return (contest_id .. problem_id):lower() end - return contest_id:lower() + return (problem_id or contest_id):lower() end M.default_filename = default_filename