New:Socket for Asana Is Now Available.Learn more
Get Started

campus-cli

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

campus-cli - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+22
tests/assignments-list.test.ts
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { test } from 'node:test';
import { isPendingAssignment } from '../src/providers/blackboard/commands/assignments.js';
test('assignments list accepts an optional courseId', () => {
const output = execFileSync(
process.execPath,
['run.js', 'assignments', 'list', '--help'],
{ cwd: process.cwd(), encoding: 'utf8' }
);
assert.match(output, /Usage: campus assignments list \[options\] \[courseId\]/);
assert.match(output, /List assignments and tasks in a course, or across all courses/);
});
test('pending filter includes only assignments without a score or submitted attempt pending grading', () => {
assert.equal(isPendingAssignment(null), true);
assert.equal(isPendingAssignment({}), true);
assert.equal(isPendingAssignment({ displayGrade: { score: 15 } }), false);
assert.equal(isPendingAssignment({ status: 'NeedsGrading' }), false);
});
+2
-1
{
"name": "campus-cli",
"version": "1.0.0",
"version": "1.0.1",
"description": "CLI/MCP no oficial para el campus universitario (Blackboard, Canvas, Moodle...) — acceso desde la terminal y MCP para IA",

@@ -14,2 +14,3 @@ "main": "run.js",

"build": "tsc",
"test": "tsx --test tests/**/*.test.ts",
"dev": "tsx src/index.ts",

@@ -16,0 +17,0 @@ "start": "node dist/index.js",

+233
-115
# campus-cli
> Accede al **campus universitario** desde la terminal o desde tu IA — sin abrir el navegador.
[![npm version](https://img.shields.io/npm/v/campus-cli.svg)](https://www.npmjs.com/package/campus-cli)
[![npm downloads](https://img.shields.io/npm/dw/campus-cli.svg)](https://www.npmjs.com/package/campus-cli)
CLI/MCP no oficial para estudiantes. Consulta cursos, descarga materiales, revisa tareas y entregas directamente desde la línea de comandos, o dale acceso a tu asistente de IA vía **MCP**. Hoy soporta **Blackboard Learn**; Canvas y Moodle están en el roadmap (ver [Providers](#providers)).
> Usa tu campus universitario desde la terminal o desde tu asistente de IA.
`campus-cli` es un CLI/MCP no oficial para estudiantes. Permite consultar cursos, tareas, notas, anuncios y materiales sin abrir el navegador. Hoy funciona con **Blackboard Learn** en UPC; Canvas y Moodle están en el roadmap.
```bash
npx campus-cli login
campus courses list
campus assignments list --pending
```
---
## Qué puedes hacer
## Instalación
- Ver tus cursos del ciclo.
- Revisar tareas pendientes, fechas de entrega y notas.
- Descargar archivos y carpetas completas de Blackboard.
- Consultar anuncios, contenidos y calificaciones.
- Usarlo desde Claude, Cursor, Copilot, Codex u otro cliente compatible con MCP.
- Automatizar consultas con `--json` o con llamadas directas a la API de Blackboard.
## Estado actual
| Universidad | LMS | Estado |
|---|---|---|
| UPC | Blackboard Learn | Implementado |
| UTP, USIL, Norbert Wiener | Canvas | Roadmap |
| UCSM, UNAP | Moodle | Roadmap |
Si estudias en una universidad con Canvas o Moodle y quieres ayudar a probar o implementar soporte, abre un issue para coordinar.
## Requisitos
- Node.js 18 o superior.
- Una cuenta activa de UPC con acceso a Aula Virtual.
- Acceso al flujo normal de Microsoft SSO, incluyendo MFA si tu cuenta lo pide.
- macOS, Linux o Windows con un entorno donde Playwright pueda abrir Chromium.
## Instalación rápida
### Usar sin instalar
```bash
# Opción 1 — usar directamente con npx (sin instalar)
npx campus-cli login
```
# Opción 2 — instalar globalmente
### Instalar globalmente
```bash
npm install -g campus-cli
campus login
```
# Opción 3 — clonar el repo
### Clonar el repo
```bash
git clone https://github.com/alejooroncoy/campus-cli
cd campus-cli
npm install # instala Chromium automáticamente (postinstall)
npm install
node run.js login
```
> Si `npm install` corrió con `--ignore-scripts` (o el navegador igual falta al hacer login), el CLI lo instala solo la primera vez que hace falta — no necesitas correr `playwright install` a mano.
`campus-cli` usa Playwright para abrir Chromium durante el login. `npm install` intenta instalar Chromium automáticamente; si el navegador falta, el CLI lo instala la primera vez que lo necesite.
---
## Primer uso
## Primeros pasos
### 1. Login
```bash

@@ -42,52 +74,47 @@ campus login

Se abre una ventana del navegador con el login de Microsoft UPC. Inicia sesión con tu cuenta `u20XXXXXXX@upc.edu.pe` (incluye MFA si lo tienes). La ventana se cierra sola y la sesión queda guardada 8 horas.
Se abrirá una ventana con el login de Microsoft UPC. Inicia sesión con tu cuenta universitaria y completa MFA si aplica.
> **Importante:** durante el login, Microsoft mostrará el mensaje **"Stay signed in?"** con un checkbox **"Don't show this again"**. Activa ese checkbox y haz clic en **Yes** — esto le indica a Microsoft que mantenga la sesión activa y es necesario para que el CLI pueda guardar las cookies correctamente.
Durante el login, Microsoft puede mostrar **"Stay signed in?"** con el checkbox **"Don't show this again"**. Marca ese checkbox y haz clic en **Yes** para que la sesión pueda mantenerse correctamente.
Después del login:
```bash
campus courses list
```
campus-cli
CLI no oficial para tu campus universitario · Blackboard · Canvas · Moodle
✓ Sesión guardada — expira en 8 horas
Usuario: Juan Pérez García
Ejemplo:
¿Qué puedo hacer ahora?
campus courses list ver tus cursos del ciclo
campus assignments list <id> ver tareas pendientes y notas
campus courses contents <id> explorar materiales
campus download-folder <id> <fid> descargar toda una carpeta
```text
_100001_1 Cálculo Diferencial e Integral [Ultra]
_100002_1 Programación Orientada a Objetos [Ultra]
_100003_1 Bases de Datos [Ultra]
_100004_1 Algoritmos y Estructuras de Datos [Ultra]
```
### 2. Ver cursos y tareas
Luego puedes revisar tareas de un curso:
```bash
campus courses list
campus assignments list _100004_1 --pending
```
_100001_1 Cálculo Diferencial e Integral [Ultra]
_100002_1 Programación Orientada a Objetos [Ultra]
_100003_1 Bases de Datos [Ultra]
_100004_1 Algoritmos y Estructuras de Datos [Ultra]
Ejemplo:
campus assignments list _100004_1
_200001_1 Tarea 1 [manual]
Nota: sin entregar · Máx: 5 pts · Entrega: 15/04/2026 (vence en 17d)
```text
_200001_1 Tarea 1 [manual]
Nota: sin entregar · Máx: 5 pts · Entrega: 15/04/2026
```
---
## Comandos principales
## Comandos
### Sesión
> Nota: los comandos son de Blackboard (único provider implementado hoy). El bin `campus` es el nombre principal — `blackboard`/`blackboard-upc` siguen funcionando como alias por compatibilidad.
### Sesión
```bash
campus login # autenticación Microsoft SSO
campus logout # borrar sesión
campus login # iniciar sesión con Microsoft SSO
campus logout # borrar sesión local
campus whoami # usuario activo y tiempo restante
campus status # versión del servidor Blackboard
campus status # sesión + versión del servidor Blackboard
```
### Cursos
```bash

@@ -97,3 +124,3 @@ campus courses list

campus courses contents <courseId>
campus courses contents <courseId> --parent <folderId> # navegar subcarpetas
campus courses contents <courseId> --parent <folderId>
campus courses contents <courseId> --type file|folder|assignment

@@ -105,37 +132,56 @@ campus courses announcements <courseId>

### Tareas
```bash
campus assignments list <courseId> # tareas con nota y fecha
campus assignments list <courseId> --pending # solo pendientes
campus assignments attempts <courseId> <id> # historial de entregas
campus assignments submit <courseId> <id> -f tarea.pdf
campus assignments submit <courseId> <id> -t "Mi respuesta" -c "Comentario"
campus assignments submit <courseId> <id> -f borrador.pdf --draft
campus assignments list <courseId>
campus assignments list
campus assignments list --pending
campus assignments list <courseId> --pending
campus assignments attempts <courseId> <assignmentId>
campus assignments submit <courseId> <assignmentId> -f tarea.pdf
campus assignments submit <courseId> <assignmentId> -t "Mi respuesta" -c "Comentario"
campus assignments submit <courseId> <assignmentId> -f borrador.pdf --draft
```
### Descargas
```bash
campus download <courseId> <contentId> # archivo individual
campus download-folder <courseId> <folderId> -o ./dir/ # carpeta completa
campus download <courseId> <contentId>
campus download-folder <courseId> <folderId> -o ./materiales/
campus download-folder <courseId> <folderId> --filter "parcial"
```
### API raw / scripting
### API y scripting
```bash
campus api GET /learn/api/public/v1/users/me
campus api GET /learn/api/public/v1/courses -q "limit=10"
campus endpoints # catálogo de todos los endpoints conocidos
campus endpoints --json # para pipelines
campus endpoints
campus endpoints --json
```
Todos los comandos aceptan `--json`. Los spinners van a `stderr`, por lo que `--json 2>/dev/null` es JSON limpio.
Todos los comandos aceptan `--json`. Los spinners van a `stderr`, así que puedes usar `--json 2>/dev/null` para obtener JSON limpio en scripts.
---
## CLI o MCP
## Uso con IA (MCP)
| Modo | Úsalo cuando quieres | Ejemplo |
|---|---|---|
| CLI | Ejecutar comandos directos desde la terminal | `campus assignments list --pending` |
| MCP | Darle acceso a tu campus a un asistente de IA | "Qué tareas tengo pendientes esta semana?" |
| API raw | Automatizar consultas o explorar endpoints | `campus api GET /learn/api/public/v1/users/me` |
`campus-cli` incluye un servidor **MCP** (Model Context Protocol) estándar — corre vía stdio con `npx campus-cli mcp`, así que funciona con cualquier cliente que hable MCP, no solo Claude. El servidor expone además un campo `instructions` (parte del handshake `initialize` de MCP) con una guía de uso, para que cualquier agente se oriente solo aunque no lea este README. Configuración probada para los más comunes:
Puedes usar ambos modos con la misma sesión. Primero ejecuta `campus login`; luego usa el CLI manualmente o conecta el servidor MCP a tu cliente de IA.
## Uso con IA mediante MCP
`campus-cli` incluye un servidor MCP estándar. Corre por `stdio` con:
```bash
npx campus-cli mcp
```
Eso permite conectar tu campus a clientes como Claude, Cursor, GitHub Copilot, OpenAI Codex CLI, Windsurf y otros clientes compatibles con Model Context Protocol.
### Claude Code
Añade a `.mcp.json` en tu proyecto:
Agrega esto a `.mcp.json`:

@@ -170,3 +216,3 @@ ```json

`Settings → MCP → Add new MCP server`, o edita directamente `~/.cursor/mcp.json` (global) o `.cursor/mcp.json` (por proyecto):
Usa `Settings -> MCP -> Add new MCP server`, o edita `~/.cursor/mcp.json`:

@@ -184,5 +230,5 @@ ```json

### GitHub Copilot (VS Code)
### GitHub Copilot en VS Code
Crea `.vscode/mcp.json` en tu proyecto (o usa el comando `MCP: Add Server` en la paleta de comandos):
Crea `.vscode/mcp.json`:

@@ -203,3 +249,3 @@ ```json

Agrega a `~/.codex/config.toml`:
Agrega esto a `~/.codex/config.toml`:

@@ -227,87 +273,159 @@ ```toml

### Otros clientes (Perplexity y similares)
Si instalaste el paquete globalmente con `npm install -g campus-cli`, puedes reemplazar `npx campus-cli` por la ruta absoluta de `campus`.
Cualquier cliente con soporte MCP sobre stdio funciona con el mismo patrón: comando `npx`, argumentos `["campus-cli", "mcp"]`. Si tu cliente no aparece aquí, revisa su documentación de "MCP servers" o "Model Context Protocol" — la configuración siempre se reduce a esos dos datos (comando + args).
### Configuración mínima
> **Nota:** Si usas instalación global (`npm install -g campus-cli`), puedes reemplazar `npx campus-cli` por la ruta absoluta del binario (`which campus`) en cualquiera de las configs de arriba.
Todos los clientes MCP terminan usando la misma idea:
### Herramientas MCP disponibles
```json
{
"command": "npx",
"args": ["campus-cli", "mcp"]
}
```
Todas las tools de Blackboard llevan el prefijo `blackboard_` (evita colisiones cuando se agreguen Canvas/Moodle):
El formato exacto cambia por cliente, pero el comando y los argumentos son los mismos.
## Herramientas MCP
Todas las herramientas actuales usan el prefijo `blackboard_` para evitar colisiones cuando se agreguen `canvas_*` o `moodle_*`.
| Herramienta | Descripción |
|---|---|
| `blackboard_whoami` | Info del estudiante autenticado |
| `blackboard_whoami` | Usuario autenticado |
| `blackboard_list_courses` | Cursos inscritos |
| `blackboard_get_course` | Detalle de un curso |
| `blackboard_list_contents` | Árbol de materiales |
| `blackboard_list_contents` | Materiales y carpetas |
| `blackboard_list_announcements` | Anuncios del curso |
| `blackboard_list_assignments` | Tareas con fechas y notas |
| `blackboard_list_attempts` | Historial de entregas |
| `blackboard_get_grades` | Notas del ciclo |
| `blackboard_list_attachments` | Archivos de un contenido |
| `blackboard_download_attachment` | Descargar archivo (base64) |
| `blackboard_submit_attempt` | Entregar tarea (pide confirmación) |
| `blackboard_get_grades` | Reporte de notas |
| `blackboard_list_attachments` | Archivos adjuntos |
| `blackboard_download_attachment` | Descargar archivo |
| `blackboard_submit_attempt` | Entregar tarea; confirma antes de enviar |
| `blackboard_get_quiz_questions` | Cargar preguntas de un quiz |
| `blackboard_save_quiz_answer` | Guardar una respuesta sin enviar |
| `blackboard_submit_quiz` | Finalizar y enviar un quiz (pide confirmación) |
| `blackboard_get_assignment_feedback` | Notas + comentarios del profesor |
| `blackboard_submit_quiz` | Finalizar y enviar un quiz; confirma antes de enviar |
| `blackboard_get_assignment_feedback` | Comentarios y feedback del profesor |
| `blackboard_raw_api` | Cualquier endpoint de Blackboard |
Con tu asistente de IA (Claude, Cursor, Copilot, Codex...) puedes hacer cosas como:
Ejemplos de uso con un asistente:
> *"¿Qué tareas tengo pendientes esta semana?"*
> *"Descárgame todos los exámenes del curso de Finanzas"*
> *"¿Cuál es mi nota actual en Arquitectura de Software?"*
```text
Qué tareas tengo pendientes esta semana?
Descarga todos los PDFs del curso de Finanzas.
Cuál es mi nota actual en Arquitectura de Software?
Busca los materiales sobre el parcial.
```
---
Ejemplo de conversación:
## Cómo funciona la autenticación
```text
Usuario: Qué tareas tengo pendientes esta semana?
IA: Tienes 2 pendientes:
- Tarea 1 de Algoritmos, vence el 15/04.
- Quiz de Bases de Datos, vence el 18/04.
```
UPC usa **SAML SSO → Microsoft Azure AD**. El CLI:
1. Abre Chromium (Playwright) en la URL SAML de UPC
2. Te muestra el login de Microsoft — tú ingresas tus credenciales
3. Captura las cookies de sesión automáticamente al redirigir a `/ultra`
4. Guarda todo en `~/.blackboard-cli/session.json` (permisos `600`)
## Seguridad y privacidad
La sesión dura **8 horas**. Después necesitas volver a hacer `login`.
- No necesitas escribir tu contraseña en la terminal.
- No hay servidor intermedio de `campus-cli`.
- Puedes cerrar sesión y borrar las cookies locales con `campus logout`.
- Es un proyecto no oficial; no está afiliado a UPC, Blackboard, Canvas ni Moodle.
- Tus credenciales se ingresan directamente en la ventana de Microsoft, no en el CLI.
- Las cookies se guardan localmente en tu máquina.
- La sesión local se guarda en `~/.blackboard-cli/session.json` con permisos restrictivos.
- No se envían cookies, credenciales ni datos del campus a servidores externos del proyecto.
- Úsalo solo con tu propia cuenta y respeta las reglas de tu universidad.
---
UPC usa SAML SSO con Microsoft Azure AD. El CLI abre Chromium con Playwright, espera a que completes el login, captura las cookies de Blackboard al volver a `/ultra` y las reutiliza para llamar la REST API.
## Stack
## Problemas comunes
- **TypeScript** + `tsx` — sin build step
- **Playwright** — maneja el flujo SAML/SSO
- **Axios** — llamadas a la REST API con cookies de sesión
- **Commander.js** — framework CLI
- **MCP SDK** — servidor MCP estándar (Claude, Cursor, Copilot, Codex...)
- **Chalk** + **Ora** — output en la terminal
### `Not authenticated`
---
Tu sesión local expiró o no existe. Ejecuta:
## Providers
```bash
campus login
```
`campus-cli` está pensado para crecer más allá de una sola universidad/LMS — la arquitectura interna (`src/providers/<lms>/`) ya separa cada proveedor en su propia carpeta (auth, api, comandos, tools MCP con su propio prefijo).
### Microsoft pide login cada vez
| Universidad | LMS | Estado |
|---|---|---|
| UPC | Blackboard Learn | ✅ Implementado (`blackboard_*`) |
| UTP, USIL, Norbert Wiener | Canvas | 🗺️ Roadmap — no implementado |
| UCSM, UNAP | Moodle | 🗺️ Roadmap — no implementado |
Cuando aparezca **"Stay signed in?"**, marca **"Don't show this again"** y responde **Yes**. Si ya habías iniciado sesión antes, prueba borrar la sesión local:
¿Tienes cuenta en una universidad con Canvas o Moodle y quieres ayudar a implementarlo? Los PRs son bienvenidos — abre un issue para coordinar antes de empezar.
```bash
campus logout
campus login
```
---
### Chromium o Playwright no abre
## Notas
Normalmente el CLI instala Chromium automáticamente. Si instalaste dependencias con scripts desactivados, vuelve a instalar:
- Probado con Blackboard Learn `v4000.10.0` (UPC, 2026).
- CLI **no oficial** — sin afiliación con ninguna universidad, Blackboard Inc., Canvas ni Moodle.
- Úsalo solo con tu propia cuenta. Respeta los TOS de tu universidad.
- Las cookies se guardan localmente. No se envían a servidores externos.
```bash
npm install
```
---
Luego intenta de nuevo:
```bash
campus login
```
### Un curso o archivo no aparece
Primero confirma que aparece en Aula Virtual desde el navegador. Si aparece en Blackboard pero no en el CLI, abre un issue con:
- Comando ejecutado.
- Si usaste `--json`.
- Tipo de contenido que falta: curso, carpeta, archivo, tarea o nota.
- Mensaje de error, si lo hubo.
No publiques cookies, tokens, capturas con datos personales ni archivos privados del curso.
## Desarrollo
```bash
npm install
npm run build
node run.js --help
```
Stack principal:
- TypeScript
- Playwright
- Axios
- Commander.js
- MCP SDK
- Chalk y Ora
La arquitectura separa cada LMS en `src/providers/<lms>/`. Blackboard vive en `src/providers/blackboard/`; futuros providers deberían seguir el mismo patrón.
## Roadmap
- Soporte para Canvas.
- Soporte para Moodle.
- Notificaciones de entregas próximas.
- Descarga de grabaciones o videos, si el LMS lo permite.
- Soporte para múltiples cuentas o ciclos.
- Más guías por cliente MCP.
Si tu universidad usa Canvas o Moodle, abre un issue con el nombre de la universidad, el LMS y qué flujo quieres probar primero: cursos, tareas, notas o materiales.
## Contribuir
Las contribuciones más útiles ahora son:
- Probar el CLI en más cursos de UPC y reportar errores con el comando usado.
- Confirmar versiones de Blackboard donde funciona o falla.
- Ayudar con soporte para Canvas o Moodle si tienes una cuenta de prueba.
- Mejorar ejemplos, screenshots, docs de instalación o configuraciones MCP.
Antes de trabajar en un provider nuevo, abre un issue para coordinar el alcance.
## Licencia
MIT
ISC

@@ -8,3 +8,3 @@ import { Command } from 'commander';

import { createClient } from '../api/client.js';
import { getMe } from '../api/courses.js';
import { getMe, getMyCourses } from '../api/courses.js';
import {

@@ -45,2 +45,38 @@ listAssignments,

export function isPendingAssignment(grade: any) {
return grade?.displayGrade?.score == null && grade?.status !== 'NeedsGrading';
}
function formatAssignment(col: any, grade: any, opts: { pending?: boolean; courseName?: string }) {
const possible = col.score?.possible ?? '?';
const due = col.grading?.due;
const attemptsAllowed = col.grading?.attemptsAllowed === 0
? 'ilimitados'
: `${col.grading?.attemptsAllowed ?? '?'} intento(s)`;
const type = col.grading?.type === 'Manual' ? chalk.gray('[manual]') : '';
let gradeStr = chalk.gray('sin entregar');
if (grade?.displayGrade?.score != null) {
const score = grade.displayGrade.score;
const pct = possible !== '?' ? Math.round((score / Number(possible)) * 100) : null;
const color = pct == null ? chalk.white : pct >= 60 ? chalk.green : chalk.red;
gradeStr = color(`${score} / ${possible}${pct != null ? ` (${pct}%)` : ''}`);
} else if (grade?.status === 'NeedsGrading') {
gradeStr = chalk.yellow('entregada — pendiente de nota');
}
if (opts.pending && !isPendingAssignment(grade)) return false;
const coursePrefix = opts.courseName ? `${chalk.gray(`[${opts.courseName}] `)}` : '';
console.log(
` ${coursePrefix}${chalk.bold(col.id)} ${chalk.cyan(col.name)} ${type}`
);
console.log(
` Nota: ${gradeStr} · Máx: ${possible} pts · ${attemptsAllowed}` +
(due ? ` · Entrega: ${formatDate(due)}${dueStatus(due)}` : '')
);
console.log('');
return true;
}
export function assignmentsCommand(program: Command) {

@@ -53,4 +89,4 @@ const assignments = program

assignments
.command('list <courseId>')
.description('List assignments and tasks in a course')
.command('list [courseId]')
.description('List assignments and tasks in a course, or across all courses')
.option('--json', 'Output raw JSON')

@@ -61,3 +97,6 @@ .option('--pending', 'Show only assignments without a submission')

const client = createClient(session);
const spinner = ora({ text: 'Fetching assignments...', stream: process.stderr }).start();
const spinner = ora({
text: courseId ? 'Fetching assignments...' : 'Fetching assignments across courses...',
stream: process.stderr,
}).start();

@@ -68,15 +107,95 @@ try {

const [columns, gradesRes] = await Promise.all([
listAssignments(client, courseId),
client
.get(`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}`, {
params: { limit: 200 },
})
.then((r) => r.data.results as any[])
.catch(() => [] as any[]),
]);
const loadCourseAssignments = async (id: string, name?: string) => {
const [columns, gradesRes] = await Promise.all([
listAssignments(client, id),
client
.get(`/learn/api/public/v1/courses/${id}/gradebook/users/${userId}`, {
params: { limit: 200 },
})
.then((r) => r.data.results as any[])
.catch(() => [] as any[]),
]);
return { courseId: id, courseName: name ?? id, columns, gradesRes };
};
if (!courseId) {
const courses = await getMyCourses(client, userId!, { limit: 100 });
const availableCourses = courses.results
.filter((uc: any) => uc.course?.availability?.available !== 'No')
.map((uc: any) => ({
id: uc.course?.id || uc.courseId,
name: uc.course?.name || uc.courseId,
}));
const results = [];
const errors: Array<{ courseId: string; courseName: string; error: string }> = [];
for (const course of availableCourses) {
spinner.text = `Fetching assignments: ${course.name}`;
try {
results.push(await loadCourseAssignments(course.id, course.name));
} catch (err: any) {
errors.push({ courseId: course.id, courseName: course.name, error: err.message });
}
}
const total = results.reduce((sum, r) => sum + r.columns.length, 0);
spinner.succeed(`${total} assignments found across ${results.length} courses`);
if (opts.json) {
const payload = results.map((r) => {
const gradeMap = new Map(r.gradesRes.map((g: any) => [g.columnId, g]));
return {
courseId: r.courseId,
courseName: r.courseName,
assignments: r.columns.filter((col) => {
const grade = gradeMap.get(col.id) ?? null;
return !opts.pending || isPendingAssignment(grade);
}),
};
});
console.log(JSON.stringify({ results: payload, errors }, null, 2));
return;
}
let printed = 0;
console.log('');
for (const result of results) {
const gradeMap = new Map(result.gradesRes.map((g: any) => [g.columnId, g]));
for (const col of result.columns) {
const grade = gradeMap.get(col.id) ?? null;
if (formatAssignment(col, grade, { pending: opts.pending, courseName: result.courseName })) {
printed++;
}
}
}
if (printed === 0) {
console.log(chalk.yellow(opts.pending
? 'No pending assignments found across your courses.'
: 'No assignments found across your courses.'
));
}
if (errors.length) {
console.log(chalk.yellow(`\nSkipped ${errors.length} course(s) due to errors:`));
errors.forEach((e) => console.log(chalk.gray(` ${e.courseName}: ${e.error}`)));
}
return;
}
const { columns, gradesRes } = await loadCourseAssignments(courseId);
spinner.succeed(`${columns.length} assignments found`);
if (opts.json) { console.log(JSON.stringify(columns, null, 2)); return; }
if (opts.json) {
const gradeMap = new Map(gradesRes.map((g: any) => [g.columnId, g]));
const filtered = columns.filter((col) => {
const grade = gradeMap.get(col.id) ?? null;
return !opts.pending || isPendingAssignment(grade);
});
console.log(JSON.stringify(filtered, null, 2));
return;
}

@@ -93,30 +212,3 @@ if (columns.length === 0) {

const grade = gradeMap.get(col.id) ?? null;
const possible = col.score?.possible ?? '?';
const due = col.grading?.due;
const attemptsAllowed = col.grading?.attemptsAllowed === 0
? 'ilimitados'
: `${col.grading?.attemptsAllowed ?? '?'} intento(s)`;
const type = col.grading?.type === 'Manual' ? chalk.gray('[manual]') : '';
// Grade display
let gradeStr = chalk.gray('sin entregar');
if (grade?.displayGrade?.score != null) {
const score = grade.displayGrade.score;
const pct = possible !== '?' ? Math.round((score / Number(possible)) * 100) : null;
const color = pct == null ? chalk.white : pct >= 60 ? chalk.green : chalk.red;
gradeStr = color(`${score} / ${possible}${pct != null ? ` (${pct}%)` : ''}`);
} else if (grade?.status === 'NeedsGrading') {
gradeStr = chalk.yellow('entregada — pendiente de nota');
}
if (opts.pending && grade?.displayGrade?.score != null) return;
console.log(
` ${chalk.bold(col.id)} ${chalk.cyan(col.name)} ${type}`
);
console.log(
` Nota: ${gradeStr} · Máx: ${possible} pts · ${attemptsAllowed}` +
(due ? ` · Entrega: ${formatDate(due)}${dueStatus(due)}` : '')
);
console.log('');
formatAssignment(col, grade, { pending: opts.pending });
});

@@ -123,0 +215,0 @@ } catch (err: any) {