@memoring/cli 0.12.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ import { apiFetch } from '../api.js';
2
+ import { fail, green, printJson, printTable, stringFlag } from './shared.js';
3
+ import { formatPercent, integerFlag, object, positiveId, readJson } from './plan-shared.js';
4
+ function examBody(parsed) {
5
+ const file = stringFlag(parsed.flags, 'file');
6
+ if (file) {
7
+ return object(readJson(file), 'a prova deve ser um objeto JSON.');
8
+ }
9
+ const name = stringFlag(parsed.flags, 'name');
10
+ const examDate = stringFlag(parsed.flags, 'date');
11
+ if (!name || !examDate) {
12
+ fail('informe --file prova.json ou --name, --date, --total e --correct.');
13
+ }
14
+ const planId = integerFlag(parsed.flags, 'plan-id', { min: 1 });
15
+ const totalQuestions = integerFlag(parsed.flags, 'total', { required: true, min: 1 });
16
+ const correctAnswers = integerFlag(parsed.flags, 'correct', { required: true, min: 0 });
17
+ const subjectsRaw = stringFlag(parsed.flags, 'subjects');
18
+ let subjectResults;
19
+ if (subjectsRaw !== undefined) {
20
+ try {
21
+ subjectResults = JSON.parse(subjectsRaw);
22
+ }
23
+ catch (error) {
24
+ fail(`--subjects deve ser um array JSON válido: ${error instanceof Error ? error.message : String(error)}`);
25
+ }
26
+ }
27
+ return {
28
+ ...(planId !== undefined ? { planId } : {}),
29
+ name,
30
+ examDate,
31
+ totalQuestions,
32
+ correctAnswers,
33
+ ...(subjectResults !== undefined ? { subjectResults } : {}),
34
+ };
35
+ }
36
+ function validateExam(value) {
37
+ if (typeof value.name !== 'string' || value.name.trim() === '') {
38
+ fail('a prova precisa de name não vazio.');
39
+ }
40
+ if (typeof value.examDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value.examDate) || Number.isNaN(Date.parse(`${value.examDate}T00:00:00Z`))) {
41
+ fail('examDate deve estar no formato YYYY-MM-DD.');
42
+ }
43
+ for (const [field, min] of [['totalQuestions', 1], ['correctAnswers', 0]]) {
44
+ const fieldValue = value[field];
45
+ if (typeof fieldValue !== 'number' || !Number.isInteger(fieldValue) || fieldValue < min) {
46
+ fail(`${field} deve ser um inteiro >= ${min}.`);
47
+ }
48
+ }
49
+ if (value.correctAnswers > value.totalQuestions) {
50
+ fail('correctAnswers não pode superar totalQuestions.');
51
+ }
52
+ if (value.planId !== undefined && value.planId !== null &&
53
+ (typeof value.planId !== 'number' || !Number.isInteger(value.planId) || value.planId < 1)) {
54
+ fail('planId deve ser um inteiro positivo.');
55
+ }
56
+ if (value.subjectResults !== undefined && !Array.isArray(value.subjectResults)) {
57
+ fail('subjectResults deve ser um array.');
58
+ }
59
+ return value;
60
+ }
61
+ export async function examsCommand(parsed, rest) {
62
+ const [sub, ...args] = rest;
63
+ switch (sub) {
64
+ case 'list': {
65
+ const exams = (await apiFetch('/planning/exams'));
66
+ if (parsed.flags.json) {
67
+ printJson(exams);
68
+ return;
69
+ }
70
+ if (exams.length === 0) {
71
+ process.stdout.write('Nenhuma prova registrada.\n');
72
+ return;
73
+ }
74
+ printTable(exams.map((exam) => ({
75
+ id: `#${String(exam.id)}`,
76
+ name: exam.name,
77
+ date: exam.examDate,
78
+ result: `${String(exam.correctAnswers)}/${String(exam.totalQuestions)}`,
79
+ accuracy: formatPercent(exam.accuracyPercent),
80
+ plan: exam.planId ? `#${String(exam.planId)}` : '—',
81
+ })), [
82
+ { key: 'id', label: 'ID' },
83
+ { key: 'name', label: 'PROVA' },
84
+ { key: 'date', label: 'DATA' },
85
+ { key: 'result', label: 'ACERTOS' },
86
+ { key: 'accuracy', label: 'DESEMPENHO' },
87
+ { key: 'plan', label: 'PLANO' },
88
+ ]);
89
+ return;
90
+ }
91
+ case 'create': {
92
+ const body = validateExam(examBody(parsed));
93
+ const exam = await apiFetch('/planning/exams', { method: 'POST', body });
94
+ if (parsed.flags.json) {
95
+ printJson(exam);
96
+ return;
97
+ }
98
+ const created = object(exam, 'resposta inválida ao criar prova.');
99
+ process.stdout.write(green(`Prova "${String(created.name)}" registrada (#${String(created.id)}).\n`));
100
+ return;
101
+ }
102
+ case 'delete': {
103
+ const examId = positiveId(args[0], 'examId');
104
+ await apiFetch(`/planning/exams/${examId}`, { method: 'DELETE' });
105
+ if (parsed.flags.json) {
106
+ printJson({ deleted: true, examId });
107
+ return;
108
+ }
109
+ process.stdout.write(green(`Prova #${examId} excluída.\n`));
110
+ return;
111
+ }
112
+ default:
113
+ process.stdout.write('Uso: memoring plan exams <list|create|delete>\n' +
114
+ ' list\n' +
115
+ ' create --file prova.json\n' +
116
+ ' create --name "Simulado 1" --date YYYY-MM-DD --total N --correct N [--plan-id N] [--subjects JSON]\n' +
117
+ ' delete <examId>\n');
118
+ if (sub) {
119
+ process.exitCode = 1;
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,131 @@
1
+ import { apiFetch } from '../api.js';
2
+ import { createDraftHandoff, deleteDraftAfterCreation, readPlanningDraft, requireReviewed, } from './planDrafts.js';
3
+ import { fail, printJson, stringFlag } from './shared.js';
4
+ import { formatPercent, integerFlag, normalized, printActivity, printGoal, } from './plan-shared.js';
5
+ import { ERROR_REASONS } from './plan-analyze.js';
6
+ export async function goalCommand(sub, planId, parsed) {
7
+ let details;
8
+ if (sub === 'current') {
9
+ details = (await apiFetch(`/planning/plans/${planId}/goals/current`));
10
+ }
11
+ else if (parsed.flags.web) {
12
+ if (parsed.flags.draft !== undefined || parsed.flags.activities !== undefined) {
13
+ fail('next --web não aceita --draft nem --activities; a Meta será definida na revisão web.');
14
+ }
15
+ await createDraftHandoff('GOAL', planId, {}, parsed);
16
+ return;
17
+ }
18
+ else {
19
+ let reviewedDraft = null;
20
+ let activityTarget;
21
+ if (parsed.flags.draft !== undefined) {
22
+ if (parsed.flags.activities !== undefined) {
23
+ fail('use --draft ou --activities, não os dois.');
24
+ }
25
+ reviewedDraft = await readPlanningDraft(stringFlag(parsed.flags, 'draft'));
26
+ requireReviewed(reviewedDraft, 'GOAL', planId);
27
+ const value = reviewedDraft.payload.activityTarget;
28
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > 200) {
29
+ fail('o payload revisado precisa de activityTarget inteiro entre 1 e 200.');
30
+ }
31
+ activityTarget = value;
32
+ }
33
+ else {
34
+ activityTarget = integerFlag(parsed.flags, 'activities', { required: true, min: 1, max: 200 });
35
+ }
36
+ details = (await apiFetch(`/planning/plans/${planId}/goals`, {
37
+ method: 'POST',
38
+ body: { activityTarget },
39
+ }));
40
+ if (reviewedDraft) {
41
+ await deleteDraftAfterCreation(reviewedDraft);
42
+ }
43
+ }
44
+ if (parsed.flags.json) {
45
+ printJson(details);
46
+ return;
47
+ }
48
+ if (!details) {
49
+ process.stdout.write(`Nenhuma Meta ativa no planejamento #${planId}. Gere uma com \`memoring plan next ${planId} --activities N\`.\n`);
50
+ return;
51
+ }
52
+ printGoal(details, sub === 'next' ? 'Meta gerada' : 'Meta atual');
53
+ }
54
+ function errorReason(raw) {
55
+ const key = normalized(raw);
56
+ const aliases = {
57
+ UNKNOWN: 'UNKNOWN',
58
+ DESCONHECIDO: 'UNKNOWN',
59
+ NAO_IDENTIFICADO: 'UNKNOWN',
60
+ FORGETTING: 'FORGETTING',
61
+ ESQUECIMENTO: 'FORGETTING',
62
+ CONCEPTUAL_CONFUSION: 'CONCEPTUAL_CONFUSION',
63
+ CONFUSAO_CONCEITUAL: 'CONCEPTUAL_CONFUSION',
64
+ INATTENTION: 'INATTENTION',
65
+ DESATENCAO: 'INATTENTION',
66
+ INTERPRETATION: 'INTERPRETATION',
67
+ INTERPRETACAO: 'INTERPRETATION',
68
+ TIME_SHORTAGE: 'TIME_SHORTAGE',
69
+ FALTA_DE_TEMPO: 'TIME_SHORTAGE',
70
+ };
71
+ const reason = aliases[key];
72
+ if (!reason) {
73
+ fail(`motivo de erro desconhecido: ${raw}. Use ${ERROR_REASONS.join(', ')}.`);
74
+ }
75
+ return reason;
76
+ }
77
+ function parseErrors(raw, incorrectCount) {
78
+ if (raw === undefined) {
79
+ return undefined;
80
+ }
81
+ const entries = raw.split(/[,;]/).map((entry) => entry.trim()).filter(Boolean);
82
+ if (entries.length === 0) {
83
+ fail('--errors vazio; use motivo:N separado por vírgulas.');
84
+ }
85
+ const seen = new Set();
86
+ const errors = entries.map((entry) => {
87
+ const match = /^(.+):(\d+)$/.exec(entry);
88
+ if (!match) {
89
+ fail(`--errors inválido em ${JSON.stringify(entry)}; use motivo:N (ex.: FORGETTING:2).`);
90
+ }
91
+ const reason = errorReason(match[1]);
92
+ const errorCount = Number(match[2]);
93
+ if (errorCount < 1) {
94
+ fail(`a contagem de ${reason} deve ser pelo menos 1.`);
95
+ }
96
+ if (seen.has(reason)) {
97
+ fail(`o motivo ${reason} aparece mais de uma vez em --errors.`);
98
+ }
99
+ seen.add(reason);
100
+ return { reason, errorCount };
101
+ });
102
+ const classified = errors.reduce((sum, error) => sum + error.errorCount, 0);
103
+ if (classified > incorrectCount) {
104
+ fail(`a soma de --errors (${classified}) não pode superar os ${incorrectCount} erros da atividade.`);
105
+ }
106
+ return errors;
107
+ }
108
+ export async function recordResult(activityId, parsed) {
109
+ const questionCount = integerFlag(parsed.flags, 'total', { required: true, min: 1 });
110
+ const correctCount = integerFlag(parsed.flags, 'correct', { required: true, min: 0 });
111
+ const actualSeconds = integerFlag(parsed.flags, 'seconds', { min: 0 });
112
+ if (correctCount > questionCount) {
113
+ fail('--correct não pode superar --total.');
114
+ }
115
+ const errors = parseErrors(stringFlag(parsed.flags, 'errors'), questionCount - correctCount);
116
+ const activity = (await apiFetch(`/planning/activities/${activityId}/result`, {
117
+ method: 'POST',
118
+ body: {
119
+ questionCount,
120
+ correctCount,
121
+ ...(actualSeconds !== undefined ? { actualSeconds } : {}),
122
+ ...(errors !== undefined ? { errors } : {}),
123
+ },
124
+ }));
125
+ if (parsed.flags.json) {
126
+ printJson(activity);
127
+ return;
128
+ }
129
+ printActivity(activity, 'Resultado registrado');
130
+ process.stdout.write(` ${correctCount}/${questionCount} corretas (${formatPercent((100 * correctCount) / questionCount)}).\n`);
131
+ }
@@ -0,0 +1,133 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { dim, fail, green, printTable, stringFlag } from './shared.js';
4
+ export const STATUS_LABELS = {
5
+ ACTIVE: 'Ativo',
6
+ ARCHIVED: 'Arquivado',
7
+ COMPLETED: 'Concluída',
8
+ PENDING: 'Pendente',
9
+ IN_PROGRESS: 'Em andamento',
10
+ POSTPONED: 'Adiada',
11
+ DISMISSED: 'Dispensada',
12
+ CANCELLED: 'Cancelada',
13
+ };
14
+ // PlanStatus da web usa COMPLETED no masculino ("planejamento concluído");
15
+ // STATUS_LABELS acima atende atividades/Metas ("concluída").
16
+ export const PLAN_STATUS_LABELS = {
17
+ ...STATUS_LABELS,
18
+ COMPLETED: 'Concluído',
19
+ };
20
+ export const TYPE_LABELS = {
21
+ THEORY: 'Teoria',
22
+ MAPPING: 'Mapeamento',
23
+ REVIEW: 'Revisão',
24
+ QUESTIONS: 'Questões',
25
+ TEST: 'Teste',
26
+ REINFORCEMENT: 'Reforço',
27
+ };
28
+ export const MATURITY_LABELS = {
29
+ BEGINNER: 'Iniciante',
30
+ INTERMEDIATE: 'Intermediário',
31
+ ADVANCED: 'Avançado',
32
+ };
33
+ export const ERROR_LABELS = {
34
+ UNKNOWN: 'Não identificado',
35
+ FORGETTING: 'Esquecimento',
36
+ CONCEPTUAL_CONFUSION: 'Confusão conceitual',
37
+ INATTENTION: 'Desatenção',
38
+ INTERPRETATION: 'Interpretação',
39
+ TIME_SHORTAGE: 'Falta de tempo',
40
+ };
41
+ export function object(value, message) {
42
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
43
+ fail(message);
44
+ }
45
+ return value;
46
+ }
47
+ export function readJson(file) {
48
+ try {
49
+ const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(resolve(file), 'utf8');
50
+ return JSON.parse(raw);
51
+ }
52
+ catch (error) {
53
+ fail(`não foi possível ler JSON de ${file === '-' ? 'stdin' : file}: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ }
56
+ export function positiveId(raw, label) {
57
+ if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1 || !Number.isSafeInteger(Number(raw))) {
58
+ fail(`${label} deve ser um inteiro positivo.`);
59
+ }
60
+ return Number(raw);
61
+ }
62
+ export function integerFlag(flags, name, options) {
63
+ const raw = stringFlag(flags, name);
64
+ if (raw === undefined) {
65
+ if (options.required) {
66
+ fail(`--${name} é obrigatório.`);
67
+ }
68
+ return undefined;
69
+ }
70
+ if (!/^-?\d+$/.test(raw)) {
71
+ fail(`--${name} deve ser um inteiro.`);
72
+ }
73
+ const value = Number(raw);
74
+ if (!Number.isSafeInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) {
75
+ fail(`--${name} deve estar entre ${options.min} e ${options.max ?? '∞'}.`);
76
+ }
77
+ return value;
78
+ }
79
+ export function normalized(value) {
80
+ return value
81
+ .normalize('NFD')
82
+ .replace(/[\u0300-\u036f]/g, '')
83
+ .trim()
84
+ .replace(/[\s-]+/g, '_')
85
+ .toUpperCase();
86
+ }
87
+ export function formatPercent(value) {
88
+ return typeof value === 'number' ? `${value.toFixed(2).replace('.', ',')}%` : '—';
89
+ }
90
+ export function formatSeconds(value) {
91
+ if (typeof value !== 'number') {
92
+ return '—';
93
+ }
94
+ if (value < 60) {
95
+ return `${Math.round(value)}s`;
96
+ }
97
+ const minutes = Math.floor(value / 60);
98
+ const seconds = Math.round(value % 60);
99
+ return minutes < 60 ? `${minutes}min${seconds ? ` ${seconds}s` : ''}` : `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`;
100
+ }
101
+ export function printActivity(activity, heading = 'Atividade atualizada') {
102
+ process.stdout.write(green(`${heading}: #${activity.id} — ${activity.title}\n`));
103
+ process.stdout.write(` tipo: ${TYPE_LABELS[activity.type] ?? activity.type} status: ${STATUS_LABELS[activity.status] ?? activity.status} estimativa: ${activity.estimatedMinutes}min\n`);
104
+ if (activity.command) {
105
+ process.stdout.write(` ${activity.command}\n`);
106
+ }
107
+ }
108
+ export function printGoal(details, heading) {
109
+ process.stdout.write(green(`${heading}: Meta #${details.goal.number} (id ${details.goal.id}) — alvo de ${details.goal.activityTarget} atividades.\n`));
110
+ if (details.activities.length === 0) {
111
+ process.stdout.write('Nenhuma atividade nesta Meta.\n');
112
+ return;
113
+ }
114
+ printTable(details.activities.map((activity) => ({
115
+ id: `#${activity.id}`,
116
+ type: TYPE_LABELS[activity.type] ?? activity.type,
117
+ title: activity.title,
118
+ status: STATUS_LABELS[activity.status] ?? activity.status,
119
+ minutes: `${activity.estimatedMinutes}min`,
120
+ questions: activity.questionTarget ?? activity.questionCount ?? '—',
121
+ })), [
122
+ { key: 'id', label: 'ID' },
123
+ { key: 'type', label: 'TIPO' },
124
+ { key: 'title', label: 'ATIVIDADE' },
125
+ { key: 'status', label: 'STATUS' },
126
+ { key: 'minutes', label: 'ESTIMATIVA' },
127
+ { key: 'questions', label: 'QUESTÕES' },
128
+ ]);
129
+ const first = details.activities.find((activity) => activity.status === 'PENDING' || activity.status === 'IN_PROGRESS');
130
+ if (first) {
131
+ process.stdout.write(dim(`\nPróxima: memoring plan start ${first.id}\n`));
132
+ }
133
+ }
@@ -0,0 +1,219 @@
1
+ import { apiFetch } from '../api.js';
2
+ import { deleteDraftAfterCreation, readPlanningDraft, requireReviewed, reviewDraft, } from './planDrafts.js';
3
+ import { PLAN_SCHEMA, analyze, planBody, planPayload } from './plan-analyze.js';
4
+ import { diagnostics } from './plan-diagnostics.js';
5
+ import { examsCommand } from './plan-exams.js';
6
+ import { goalCommand, recordResult } from './plan-goal.js';
7
+ import { MATURITY_LABELS, PLAN_STATUS_LABELS, STATUS_LABELS, formatPercent, object, positiveId, printActivity, } from './plan-shared.js';
8
+ import { bold, fail, green, printJson, printTable, stringFlag } from './shared.js';
9
+ /**
10
+ * `memoring plan` — dispatcher dos subcomandos do Planejamento. O módulo
11
+ * ficou grande demais como arquivo único; cada família coesa vive no seu
12
+ * próprio módulo: `plan-analyze` (schema/analyze/create payload), `plan-goal`
13
+ * (current/next/start/complete/postpone/dismiss/cancel/result), `plan-exams`,
14
+ * `plan-diagnostics` e `plan-shared` (rótulos, parsers e impressão).
15
+ */
16
+ function printHelp() {
17
+ process.stdout.write(`Uso: memoring plan <subcomando> [argumentos] [flags]
18
+
19
+ schema contrato JSON editável do planejamento e das provas
20
+ analyze <pdfs...> analisa PDFs no backend e aguarda a proposta
21
+ [--out proposta.json] salva o rascunho (com --json, imprime só o rascunho)
22
+ [--web] [--no-open] envia a proposta para revisão web, sem criar o plano
23
+ review <draftId> [--wait] lê o rascunho; --wait aguarda REVIEWED
24
+ [--timeout N] [--out f] timeout em segundos; exporta o payload já REVIEWED
25
+ create --file proposta.json cria o planejamento após sua revisão no arquivo
26
+ create --draft <uuid> cria somente de um rascunho PLAN REVIEWED e o exclui
27
+ list lista planejamentos
28
+ show <planId> detalha disciplinas e assuntos
29
+ current <planId> mostra a Meta ativa
30
+ next <planId> --activities N gera a próxima Meta (activityTarget obrigatório)
31
+ next <planId> --web cria handoff de Meta para revisão web, sem gerar a Meta
32
+ next <planId> --draft <uuid> gera de um rascunho GOAL REVIEWED e o exclui
33
+ start|complete|postpone|dismiss|cancel <activityId>
34
+ transições de uma atividade
35
+ (cancel = redução definitiva de escopo:
36
+ encerra na Meta e nunca é recomposta)
37
+ result <activityId> --total N --correct N [--seconds N]
38
+ [--errors motivo:N,...] registra desempenho e motivos dos erros
39
+ tec-result <activityId> importa resultado do caderno TEC já disponível
40
+ performance|error-patterns|comparison|journey <planId>
41
+ diagnósticos do planejamento
42
+ coordinates coordenadas do método de planejamento
43
+ exams list lista provas
44
+ exams create --file prova.json registra prova (ou use --name/--date/--total/--correct)
45
+ exams delete <examId> exclui prova
46
+
47
+ Maturidades: BEGINNER (Iniciante), INTERMEDIATE (Intermediário), ADVANCED (Avançado).
48
+ Motivos de erro: UNKNOWN, FORGETTING, CONCEPTUAL_CONFUSION, INATTENTION, INTERPRETATION, TIME_SHORTAGE (aliases em PT-BR também são aceitos).
49
+ `);
50
+ }
51
+ export async function planCommand(parsed) {
52
+ const [sub, ...rest] = parsed._;
53
+ switch (sub) {
54
+ case 'schema':
55
+ printJson(PLAN_SCHEMA);
56
+ return;
57
+ case 'analyze':
58
+ await analyze(parsed, rest);
59
+ return;
60
+ case 'review':
61
+ case 'draft':
62
+ await reviewDraft(parsed, rest[0]);
63
+ return;
64
+ case 'create': {
65
+ const file = stringFlag(parsed.flags, 'file');
66
+ if (file && parsed.flags.draft !== undefined) {
67
+ fail('use --file ou --draft, não os dois.');
68
+ }
69
+ let body;
70
+ let reviewedDraft = null;
71
+ if (parsed.flags.draft !== undefined) {
72
+ reviewedDraft = await readPlanningDraft(stringFlag(parsed.flags, 'draft'));
73
+ requireReviewed(reviewedDraft, 'PLAN');
74
+ body = planPayload(reviewedDraft.payload);
75
+ }
76
+ else if (file) {
77
+ body = planBody(file);
78
+ }
79
+ else {
80
+ fail('informe a proposta revisada: memoring plan create --file proposta.json ou --draft <uuid>.');
81
+ }
82
+ const details = (await apiFetch('/planning/plans', { method: 'POST', body }));
83
+ if (reviewedDraft) {
84
+ await deleteDraftAfterCreation(reviewedDraft);
85
+ }
86
+ if (parsed.flags.json) {
87
+ printJson(details);
88
+ return;
89
+ }
90
+ const plan = object(details.plan, 'resposta inválida ao criar planejamento.');
91
+ process.stdout.write(green(`Planejamento "${String(plan.name)}" criado (#${String(plan.id)}).\n`));
92
+ process.stdout.write(`Gere a primeira Meta: memoring plan next ${String(plan.id)} --activities N\n`);
93
+ return;
94
+ }
95
+ case 'list': {
96
+ const plans = (await apiFetch('/planning/plans'));
97
+ if (parsed.flags.json) {
98
+ printJson(plans);
99
+ return;
100
+ }
101
+ if (plans.length === 0) {
102
+ process.stdout.write('Nenhum planejamento. Analise PDFs com `memoring plan analyze <pdfs...>`.\n');
103
+ return;
104
+ }
105
+ printTable(plans.map((plan) => ({
106
+ id: `#${String(plan.id)}`,
107
+ name: plan.name,
108
+ status: PLAN_STATUS_LABELS[String(plan.status)] ?? plan.status,
109
+ subjects: plan.activeSubjectLimit,
110
+ weekly: plan.weeklyMinutes ? `${String(plan.weeklyMinutes)}min` : '—',
111
+ })), [
112
+ { key: 'id', label: 'ID' },
113
+ { key: 'name', label: 'PLANEJAMENTO' },
114
+ { key: 'status', label: 'STATUS' },
115
+ { key: 'subjects', label: 'DISCIPLINAS ATIVAS' },
116
+ { key: 'weekly', label: 'TEMPO SEMANAL' },
117
+ ]);
118
+ return;
119
+ }
120
+ case 'show': {
121
+ const planId = positiveId(rest[0], 'planId');
122
+ const details = (await apiFetch(`/planning/plans/${planId}`));
123
+ if (parsed.flags.json) {
124
+ printJson(details);
125
+ return;
126
+ }
127
+ const plan = object(details.plan, 'resposta inválida ao consultar planejamento.');
128
+ process.stdout.write(bold(`${String(plan.name)} (#${planId}) — ${PLAN_STATUS_LABELS[String(plan.status)] ?? String(plan.status)}\n`));
129
+ if (plan.objective) {
130
+ process.stdout.write(`${String(plan.objective)}\n`);
131
+ }
132
+ const programs = Array.isArray(details.programs) ? details.programs : [];
133
+ const topics = Array.isArray(details.topics) ? details.topics : [];
134
+ for (const program of programs) {
135
+ process.stdout.write(`\n${String(program.subjectName)} — ${MATURITY_LABELS[String(program.maturity)] ?? String(program.maturity)}\n`);
136
+ const programTopics = topics.filter((topic) => topic.programId === program.id);
137
+ printTable(programTopics.map((topic) => ({
138
+ id: `#${String(topic.id)}`,
139
+ title: topic.title,
140
+ pages: topic.pageFrom ? `${String(topic.pageFrom)}${topic.pageTo !== topic.pageFrom ? `–${String(topic.pageTo)}` : ''}` : '—',
141
+ minutes: `${String(topic.estimatedMinutes)}min`,
142
+ status: STATUS_LABELS[String(topic.status)] ?? topic.status,
143
+ })), [
144
+ { key: 'id', label: 'ID' },
145
+ { key: 'title', label: 'ASSUNTO' },
146
+ { key: 'pages', label: 'PÁGINAS' },
147
+ { key: 'minutes', label: 'ESTIMATIVA' },
148
+ { key: 'status', label: 'STATUS' },
149
+ ]);
150
+ }
151
+ return;
152
+ }
153
+ case 'current':
154
+ case 'next':
155
+ await goalCommand(sub, positiveId(rest[0], 'planId'), parsed);
156
+ return;
157
+ case 'start':
158
+ case 'complete':
159
+ case 'postpone':
160
+ case 'dismiss':
161
+ case 'cancel': {
162
+ const activityId = positiveId(rest[0], 'activityId');
163
+ const activity = (await apiFetch(`/planning/activities/${activityId}/${sub}`, { method: 'POST' }));
164
+ if (parsed.flags.json) {
165
+ printJson(activity);
166
+ }
167
+ else {
168
+ printActivity(activity);
169
+ }
170
+ return;
171
+ }
172
+ case 'result':
173
+ await recordResult(positiveId(rest[0], 'activityId'), parsed);
174
+ return;
175
+ case 'tec-result': {
176
+ const activityId = positiveId(rest[0], 'activityId');
177
+ const result = (await apiFetch(`/planning/activities/${activityId}/tec-result`, { method: 'POST' }));
178
+ if (parsed.flags.json) {
179
+ printJson(result);
180
+ return;
181
+ }
182
+ process.stdout.write(green(`Resultado TEC importado para a atividade #${activityId}.\n`));
183
+ process.stdout.write(` ${String(result.correctCount)}/${String(result.questionCount)} corretas (${formatPercent(result.accuracyPercent)}); ` +
184
+ `${String(result.errorCount)} erros.\n`);
185
+ return;
186
+ }
187
+ case 'performance':
188
+ case 'error-patterns':
189
+ case 'comparison':
190
+ case 'journey':
191
+ await diagnostics(sub, positiveId(rest[0], 'planId'), parsed.flags.json === true);
192
+ return;
193
+ case 'coordinates': {
194
+ const guides = (await apiFetch('/planning/coordinates'));
195
+ if (parsed.flags.json) {
196
+ printJson(guides);
197
+ return;
198
+ }
199
+ for (const guide of guides) {
200
+ process.stdout.write(bold(`${String(guide.title)}\n`));
201
+ process.stdout.write(`${String(guide.description)}\n`);
202
+ const actions = Array.isArray(guide.actions) ? guide.actions : [];
203
+ for (const action of actions) {
204
+ process.stdout.write(` • ${String(action)}\n`);
205
+ }
206
+ process.stdout.write('\n');
207
+ }
208
+ return;
209
+ }
210
+ case 'exams':
211
+ await examsCommand(parsed, rest);
212
+ return;
213
+ default:
214
+ printHelp();
215
+ if (sub) {
216
+ process.exitCode = 1;
217
+ }
218
+ }
219
+ }