campus-cli
Advanced tools
| 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. | ||
| [](https://www.npmjs.com/package/campus-cli) | ||
| [](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) { |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
145599
4.44%24
4.35%2745
3.74%0
-100%425
38.44%3
50%