minerva-cli 0.1.0 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { Command as Command20 } from "commander";
9
9
  // src/config/constants.ts
10
10
  var APP_NAME = "minerva";
11
11
  var APP_DESCRIPTION = "Organiza\xE7\xE3o acad\xEAmica para estudantes de ADS da FATEC \u{1F989}";
12
- var APP_VERSION = "0.1.0";
12
+ var APP_VERSION = "0.2.1";
13
13
  var DEFAULT_VAULT_DIRNAME = "fatec-ads";
14
14
  var MATRIZ_URL = "https://fatectq.cps.sp.gov.br/matriz-curricular-ads/";
15
15
 
@@ -571,14 +571,20 @@ function disciplinaDir(root, semestre, disciplina) {
571
571
  async function scaffoldDisciplina(root, semestre, disciplina) {
572
572
  const dir = disciplinaDir(root, semestre, disciplina.nome);
573
573
  await Promise.all(
574
- DISCIPLINA_FOLDERS.map((folder) => fs5.ensureDir(path6.join(dir, folder)))
574
+ DISCIPLINA_FOLDERS.map(
575
+ (folder) => fs5.ensureDir(path6.join(dir, folder))
576
+ )
575
577
  );
576
578
  const files = [
577
579
  [
578
580
  "README.md",
579
581
  () => renderTemplate(
580
582
  "disciplina",
581
- { disciplina: disciplina.nome, semestre, aulas: disciplina.aulas },
583
+ {
584
+ disciplina: disciplina.nome,
585
+ semestre,
586
+ aulas: disciplina.aulas
587
+ },
582
588
  root
583
589
  )
584
590
  ],
@@ -831,7 +837,9 @@ function doctorCommand() {
831
837
  {
832
838
  label: "Node.js",
833
839
  run: () => {
834
- const major = Number(process.versions.node.split(".")[0]);
840
+ const major = Number(
841
+ process.versions.node.split(".")[0]
842
+ );
835
843
  if (major < 20) {
836
844
  throw new Error(
837
845
  `v${process.versions.node} \u2014 atualize para a LTS (>= 20)`
@@ -848,7 +856,9 @@ function doctorCommand() {
848
856
  label: "Vault Minerva",
849
857
  run: () => {
850
858
  if (!root) {
851
- throw new Error('n\xE3o encontrado \u2014 rode "minerva init"');
859
+ throw new Error(
860
+ 'n\xE3o encontrado \u2014 rode "minerva init"'
861
+ );
852
862
  }
853
863
  return Promise.resolve(root);
854
864
  }
@@ -878,7 +888,9 @@ function doctorCommand() {
878
888
  label: "Reposit\xF3rio Git",
879
889
  run: async () => {
880
890
  if (!await fs7.pathExists(path10.join(root, ".git"))) {
881
- throw new Error('ausente \u2014 rode "git init" no vault');
891
+ throw new Error(
892
+ 'ausente \u2014 rode "git init" no vault'
893
+ );
882
894
  }
883
895
  return "ok";
884
896
  }
@@ -886,7 +898,9 @@ function doctorCommand() {
886
898
  {
887
899
  label: "Vault do Obsidian",
888
900
  run: async () => {
889
- if (!await fs7.pathExists(path10.join(root, ".obsidian"))) {
901
+ if (!await fs7.pathExists(
902
+ path10.join(root, ".obsidian")
903
+ )) {
890
904
  throw new Error(
891
905
  'pasta .obsidian ausente \u2014 rode "minerva init" novamente'
892
906
  );
@@ -939,7 +953,9 @@ async function loadEstudoLog(root) {
939
953
  }
940
954
  async function registrarEstudo(vault, horas, disciplina) {
941
955
  if (!Number.isFinite(horas) || horas <= 0) {
942
- throw new Error("Informe um n\xFAmero de horas maior que zero (ex.: 1.5).");
956
+ throw new Error(
957
+ "Informe um n\xFAmero de horas maior que zero (ex.: 1.5)."
958
+ );
943
959
  }
944
960
  const sessao = {
945
961
  data: todayISO(),
@@ -985,12 +1001,16 @@ function estudoCommand() {
985
1001
  );
986
1002
  return;
987
1003
  }
988
- logger.title(`${vault.config.horasEstudadas}h estudadas no total`);
1004
+ logger.title(
1005
+ `${vault.config.horasEstudadas}h estudadas no total`
1006
+ );
989
1007
  for (const [disciplina2, total] of totals) {
990
1008
  logger.item(`${disciplina2}: ${total}h`);
991
1009
  }
992
1010
  if (totals.size === 0) {
993
- logger.info("Registre sua primeira sess\xE3o: minerva estudo 1.5");
1011
+ logger.info(
1012
+ "Registre sua primeira sess\xE3o: minerva estudo 1.5"
1013
+ );
994
1014
  }
995
1015
  return;
996
1016
  }
@@ -1039,7 +1059,9 @@ async function collectMarkdownFiles(dir) {
1039
1059
  const stack = [dir];
1040
1060
  while (stack.length > 0) {
1041
1061
  const current = stack.pop();
1042
- for (const entry of await fs9.readdir(current, { withFileTypes: true })) {
1062
+ for (const entry of await fs9.readdir(current, {
1063
+ withFileTypes: true
1064
+ })) {
1043
1065
  const full = path12.join(current, entry.name);
1044
1066
  if (entry.isDirectory()) stack.push(full);
1045
1067
  else if (entry.name.endsWith(".md")) files.push(full);
@@ -1052,14 +1074,22 @@ async function collectAllFlashcards(vault) {
1052
1074
  const stack = [vault.root];
1053
1075
  while (stack.length > 0) {
1054
1076
  const current = stack.pop();
1055
- for (const entry of await fs9.readdir(current, { withFileTypes: true })) {
1056
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1077
+ for (const entry of await fs9.readdir(current, {
1078
+ withFileTypes: true
1079
+ })) {
1080
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
1081
+ continue;
1057
1082
  const full = path12.join(current, entry.name);
1058
1083
  if (!entry.isDirectory()) continue;
1059
1084
  if (entry.name.toLowerCase() === "flashcards") {
1060
1085
  const deck = path12.basename(path12.dirname(full));
1061
1086
  for (const file of await collectMarkdownFiles(full)) {
1062
- cards.push(...parseFlashcards(await fs9.readFile(file, "utf8"), deck));
1087
+ cards.push(
1088
+ ...parseFlashcards(
1089
+ await fs9.readFile(file, "utf8"),
1090
+ deck
1091
+ )
1092
+ );
1063
1093
  }
1064
1094
  } else {
1065
1095
  stack.push(full);
@@ -1134,22 +1164,26 @@ async function createNote(vault, input5) {
1134
1164
 
1135
1165
  // src/services/ai.ts
1136
1166
  import Anthropic from "@anthropic-ai/sdk";
1137
- var AI_MODEL = "claude-opus-4-8";
1167
+ var ANTHROPIC_MODEL = "claude-opus-4-8";
1168
+ var OLLAMA_DEFAULT_MODEL = "llama3.2";
1169
+ var OLLAMA_DEFAULT_URL = "http://localhost:11434";
1170
+ var SYSTEM_PROMPT = "Voc\xEA \xE9 um monitor de estudos da FATEC (curso de An\xE1lise e Desenvolvimento de Sistemas). Responda sempre em portugu\xEAs do Brasil, em Markdown puro, sem pre\xE2mbulos.";
1138
1171
  function anthropicProvider() {
1139
1172
  if (!process.env["ANTHROPIC_API_KEY"]) {
1140
1173
  throw new Error(
1141
- 'Defina a vari\xE1vel de ambiente ANTHROPIC_API_KEY para usar os recursos de IA.\n Ex.: export ANTHROPIC_API_KEY="sk-ant-..."'
1174
+ 'Defina a vari\xE1vel de ambiente ANTHROPIC_API_KEY para usar o provedor Anthropic.\n Ex.: export ANTHROPIC_API_KEY="sk-ant-..."'
1142
1175
  );
1143
1176
  }
1144
1177
  const client = new Anthropic();
1145
1178
  return {
1179
+ name: `anthropic (${ANTHROPIC_MODEL})`,
1146
1180
  async generate(prompt, material) {
1147
1181
  try {
1148
1182
  const stream = client.messages.stream({
1149
- model: AI_MODEL,
1183
+ model: ANTHROPIC_MODEL,
1150
1184
  max_tokens: 64e3,
1151
1185
  thinking: { type: "adaptive" },
1152
- system: "Voc\xEA \xE9 um monitor de estudos da FATEC (curso de An\xE1lise e Desenvolvimento de Sistemas). Responda sempre em portugu\xEAs do Brasil, em Markdown puro, sem pre\xE2mbulos.",
1186
+ system: SYSTEM_PROMPT,
1153
1187
  messages: [
1154
1188
  {
1155
1189
  role: "user",
@@ -1165,7 +1199,9 @@ ${material}
1165
1199
  return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
1166
1200
  } catch (error) {
1167
1201
  if (error instanceof Anthropic.AuthenticationError) {
1168
- throw new Error("ANTHROPIC_API_KEY inv\xE1lida. Verifique a chave.");
1202
+ throw new Error(
1203
+ "ANTHROPIC_API_KEY inv\xE1lida. Verifique a chave."
1204
+ );
1169
1205
  }
1170
1206
  if (error instanceof Anthropic.RateLimitError) {
1171
1207
  throw new Error(
@@ -1182,6 +1218,110 @@ ${material}
1182
1218
  }
1183
1219
  };
1184
1220
  }
1221
+ function ollamaProvider() {
1222
+ const baseUrl = (process.env["OLLAMA_URL"] ?? OLLAMA_DEFAULT_URL).replace(
1223
+ /\/$/,
1224
+ ""
1225
+ );
1226
+ const model = process.env["OLLAMA_MODEL"] ?? OLLAMA_DEFAULT_MODEL;
1227
+ const headers = {
1228
+ "content-type": "application/json"
1229
+ };
1230
+ if (process.env["OLLAMA_API_KEY"]) {
1231
+ headers["authorization"] = `Bearer ${process.env["OLLAMA_API_KEY"]}`;
1232
+ }
1233
+ const options = {
1234
+ num_predict: Number(process.env["OLLAMA_MAX_TOKENS"] ?? 1500)
1235
+ };
1236
+ if (process.env["OLLAMA_THREADS"]) {
1237
+ options["num_thread"] = Number(process.env["OLLAMA_THREADS"]);
1238
+ }
1239
+ return {
1240
+ name: `ollama (${model} @ ${baseUrl})`,
1241
+ async generate(prompt, material) {
1242
+ let response;
1243
+ try {
1244
+ response = await fetch(`${baseUrl}/api/chat`, {
1245
+ method: "POST",
1246
+ headers,
1247
+ body: JSON.stringify({
1248
+ model,
1249
+ stream: false,
1250
+ options,
1251
+ messages: [
1252
+ { role: "system", content: SYSTEM_PROMPT },
1253
+ {
1254
+ role: "user",
1255
+ content: `${prompt}
1256
+
1257
+ <material>
1258
+ ${material}
1259
+ </material>`
1260
+ }
1261
+ ]
1262
+ })
1263
+ });
1264
+ } catch (cause) {
1265
+ throw new Error(
1266
+ `N\xE3o consegui falar com o Ollama em ${baseUrl}.
1267
+ Local: instale com "curl -fsSL https://ollama.com/install.sh | sh" e rode "ollama serve".
1268
+ VPS: defina OLLAMA_URL apontando para o seu servidor.`,
1269
+ { cause }
1270
+ );
1271
+ }
1272
+ if (response.status === 404) {
1273
+ throw new Error(
1274
+ `O modelo "${model}" n\xE3o est\xE1 instalado no Ollama. Rode: ollama pull ${model}`
1275
+ );
1276
+ }
1277
+ if (!response.ok) {
1278
+ throw new Error(
1279
+ `Ollama respondeu HTTP ${response.status}: ${await response.text()}`
1280
+ );
1281
+ }
1282
+ const data = await response.json();
1283
+ const content = data.message?.content;
1284
+ if (!content) {
1285
+ throw new Error("O Ollama retornou uma resposta vazia.");
1286
+ }
1287
+ return content;
1288
+ }
1289
+ };
1290
+ }
1291
+ async function ollamaDisponivel() {
1292
+ const baseUrl = (process.env["OLLAMA_URL"] ?? OLLAMA_DEFAULT_URL).replace(
1293
+ /\/$/,
1294
+ ""
1295
+ );
1296
+ const headers = {};
1297
+ if (process.env["OLLAMA_API_KEY"]) {
1298
+ headers["authorization"] = `Bearer ${process.env["OLLAMA_API_KEY"]}`;
1299
+ }
1300
+ try {
1301
+ const response = await fetch(`${baseUrl}/api/tags`, {
1302
+ headers,
1303
+ signal: AbortSignal.timeout(3e3)
1304
+ });
1305
+ return response.ok;
1306
+ } catch {
1307
+ return false;
1308
+ }
1309
+ }
1310
+ async function resolveProvider() {
1311
+ const forced = process.env["MINERVA_AI"]?.toLowerCase();
1312
+ if (forced === "anthropic") return anthropicProvider();
1313
+ if (forced === "ollama") return ollamaProvider();
1314
+ if (forced) {
1315
+ throw new Error(
1316
+ `Provedor desconhecido em MINERVA_AI: "${forced}". Use "anthropic" ou "ollama".`
1317
+ );
1318
+ }
1319
+ if (process.env["ANTHROPIC_API_KEY"]) return anthropicProvider();
1320
+ if (await ollamaDisponivel()) return ollamaProvider();
1321
+ throw new Error(
1322
+ 'Nenhum provedor de IA dispon\xEDvel. Escolha um:\n \u2022 Anthropic (pago): export ANTHROPIC_API_KEY="sk-ant-..."\n \u2022 Ollama (gr\xE1tis, local): instale em https://ollama.com e rode "ollama pull llama3.2"\n \u2022 Ollama (gr\xE1tis, na sua VPS): export OLLAMA_URL="http://SEU-IP:11434"'
1323
+ );
1324
+ }
1185
1325
  function resumoPrompt(disciplina, tema) {
1186
1326
  return `Gere um resumo de estudo sobre "${tema}" para a disciplina "${disciplina}", baseado nas anota\xE7\xF5es de aula fornecidas no material. Estruture com: vis\xE3o geral, pontos principais numerados, explica\xE7\xE3o com palavras simples e uma se\xE7\xE3o "Poss\xEDveis quest\xF5es de prova" com checkboxes (- [ ]). Se o material for insuficiente, complemente com seu conhecimento e sinalize o que n\xE3o veio das anota\xE7\xF5es.`;
1187
1327
  }
@@ -1206,7 +1346,9 @@ var DATE_HINT = "Use uma data v\xE1lida no formato YYYY-MM-DD.";
1206
1346
  async function readMaterial(dir) {
1207
1347
  if (!await fs11.pathExists(dir)) return "";
1208
1348
  const parts = [];
1209
- for (const file of (await fs11.readdir(dir)).filter((f) => f.endsWith(".md"))) {
1349
+ for (const file of (await fs11.readdir(dir)).filter(
1350
+ (f) => f.endsWith(".md")
1351
+ )) {
1210
1352
  parts.push(await fs11.readFile(path14.join(dir, file), "utf8"));
1211
1353
  }
1212
1354
  return parts.join("\n\n---\n\n");
@@ -1222,7 +1364,7 @@ function noteCommand(spec) {
1222
1364
  if (spec.ai) {
1223
1365
  command.option(
1224
1366
  "--ai",
1225
- `gera o conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/ (requer ANTHROPIC_API_KEY)`
1367
+ `gera o conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/ (Anthropic ou Ollama)`
1226
1368
  );
1227
1369
  }
1228
1370
  command.action(
@@ -1256,13 +1398,13 @@ function noteCommand(spec) {
1256
1398
  }
1257
1399
  let conteudoIA;
1258
1400
  if (spec.ai && options["ai"]) {
1259
- const provider = anthropicProvider();
1401
+ const provider = await resolveProvider();
1260
1402
  const sourceDir = path14.join(
1261
1403
  disciplinaDir(vault.root, semestre, disciplina),
1262
1404
  spec.ai.sourceFolder
1263
1405
  );
1264
1406
  const spinAi = spinner(
1265
- `Gerando conte\xFAdo com IA (${spec.ai.sourceFolder}/)...`
1407
+ `Gerando conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/...`
1266
1408
  ).start();
1267
1409
  try {
1268
1410
  const material = await readMaterial(sourceDir);
@@ -1286,11 +1428,14 @@ function noteCommand(spec) {
1286
1428
  extra
1287
1429
  });
1288
1430
  if (conteudoIA) {
1289
- await fs11.appendFile(file, `
1431
+ await fs11.appendFile(
1432
+ file,
1433
+ `
1290
1434
  ## Gerado por IA
1291
1435
 
1292
1436
  ${conteudoIA}
1293
- `);
1437
+ `
1438
+ );
1294
1439
  }
1295
1440
  spin.succeed(`Pronto! Nota criada em:`);
1296
1441
  logger.item(path14.relative(vault.root, file));
@@ -1384,14 +1529,18 @@ function flashcardsCommand() {
1384
1529
  );
1385
1530
  const create = flashcardsNewCommand();
1386
1531
  command.addCommand(create, { isDefault: true });
1387
- command.command("export").description("Exporta todos os flashcards para um TSV import\xE1vel no Anki").action(async () => {
1532
+ command.command("export").description(
1533
+ "Exporta todos os flashcards para um TSV import\xE1vel no Anki"
1534
+ ).action(async () => {
1388
1535
  const vault = await requireVault();
1389
1536
  const spin = spinner("Coletando flashcards do vault...").start();
1390
1537
  try {
1391
1538
  const { file, total } = await exportAnki(vault);
1392
1539
  spin.succeed(`${total} flashcard(s) exportado(s)!`);
1393
1540
  logger.item(path15.relative(vault.root, file));
1394
- logger.info("No Anki: Arquivo \u2192 Importar e selecione esse arquivo.");
1541
+ logger.info(
1542
+ "No Anki: Arquivo \u2192 Importar e selecione esse arquivo."
1543
+ );
1395
1544
  } catch (error) {
1396
1545
  spin.fail("Falha na exporta\xE7\xE3o.");
1397
1546
  throw error;
@@ -1442,7 +1591,10 @@ function initCommand() {
1442
1591
  return new Command11("init").description("Cria a estrutura completa do curso (vault de estudos)").argument("[pasta]", "pasta de destino", DEFAULT_VAULT_DIRNAME).option("-s, --semestre <n>", "semestre atual (1-6)").option("-a, --aluno <nome>", "seu nome").action(
1443
1592
  async (pasta, options) => {
1444
1593
  logger.title("Minerva CLI \u2014 vamos montar sua gradua\xE7\xE3o!");
1445
- const aluno = options.aluno ?? await input3({ message: "Seu nome:", default: "Estudante" });
1594
+ const aluno = options.aluno ?? await input3({
1595
+ message: "Seu nome:",
1596
+ default: "Estudante"
1597
+ });
1446
1598
  const semestreAtual = options.semestre !== void 0 ? Number(options.semestre) : await number2({
1447
1599
  message: "Em qual semestre voc\xEA est\xE1? (1-6)",
1448
1600
  default: 1,
@@ -1467,7 +1619,10 @@ function initCommand() {
1467
1619
  spin.text = "Copiando templates...";
1468
1620
  await scaffoldTemplates(root);
1469
1621
  spin.text = "Criando semestres e disciplinas...";
1470
- const totalDisciplinas = await scaffoldSemestres(root, matrizOficial);
1622
+ const totalDisciplinas = await scaffoldSemestres(
1623
+ root,
1624
+ matrizOficial
1625
+ );
1471
1626
  spin.text = "Preparando vault do Obsidian...";
1472
1627
  await scaffoldObsidian(root);
1473
1628
  spin.text = "Gerando README e dashboard...";
@@ -1480,7 +1635,10 @@ function initCommand() {
1480
1635
  semestres: matrizOficial.semestres
1481
1636
  })
1482
1637
  );
1483
- await writeIfMissing(path16.join(root, ".gitignore"), GITIGNORE);
1638
+ await writeIfMissing(
1639
+ path16.join(root, ".gitignore"),
1640
+ GITIGNORE
1641
+ );
1484
1642
  await writeIfMissing(
1485
1643
  path16.join(root, "roadmap", "Roadmap.md"),
1486
1644
  ROADMAP
@@ -1491,14 +1649,20 @@ function initCommand() {
1491
1649
  spin.succeed("Vault criado com sucesso!");
1492
1650
  logger.blank();
1493
1651
  logger.success(`\u{1F4C1} ${root}`);
1494
- logger.item(`${totalDisciplinas} disciplinas em 6 semestres`);
1652
+ logger.item(
1653
+ `${totalDisciplinas} disciplinas em 6 semestres`
1654
+ );
1495
1655
  logger.item("Dashboard em dashboard/Dashboard.md");
1496
1656
  logger.item("Abra a pasta como vault no Obsidian \u{1F7E3}");
1497
1657
  if (!gitOk) {
1498
- logger.warn("Git n\xE3o encontrado \u2014 reposit\xF3rio n\xE3o inicializado.");
1658
+ logger.warn(
1659
+ "Git n\xE3o encontrado \u2014 reposit\xF3rio n\xE3o inicializado."
1660
+ );
1499
1661
  }
1500
1662
  logger.blank();
1501
- logger.info(`Pr\xF3ximo passo: cd ${pasta} && minerva dashboard`);
1663
+ logger.info(
1664
+ `Pr\xF3ximo passo: cd ${pasta} && minerva dashboard`
1665
+ );
1502
1666
  } catch (error) {
1503
1667
  spin.fail("Falha ao criar o vault.");
1504
1668
  throw error;
@@ -1593,7 +1757,9 @@ function notaCommand() {
1593
1757
  `${linha.disciplina}: m\xE9dia ${chalk2.bold(linha.media)} \u2014 ${color(linha.situacao)}`
1594
1758
  );
1595
1759
  for (const nota of linha.notas) {
1596
- logger.item(`${nota.avaliacao}: ${nota.valor} (peso ${nota.peso})`);
1760
+ logger.item(
1761
+ `${nota.avaliacao}: ${nota.valor} (peso ${nota.peso})`
1762
+ );
1597
1763
  }
1598
1764
  }
1599
1765
  });
@@ -1627,7 +1793,9 @@ function notaCommand() {
1627
1793
  peso: Number(options.peso)
1628
1794
  });
1629
1795
  const notas = await loadNotas(vault.root);
1630
- const linha = boletim(notas).find((b) => b.disciplina === disciplina);
1796
+ const linha = boletim(notas).find(
1797
+ (b) => b.disciplina === disciplina
1798
+ );
1631
1799
  logger.success(
1632
1800
  `${avaliacao} = ${valorFinal} registrada em ${disciplina}.`
1633
1801
  );
@@ -1702,7 +1870,9 @@ async function listPendencias(vault, matriz) {
1702
1870
  const stack = [dir];
1703
1871
  while (stack.length > 0) {
1704
1872
  const current = stack.pop();
1705
- for (const entry of await fs14.readdir(current, { withFileTypes: true })) {
1873
+ for (const entry of await fs14.readdir(current, {
1874
+ withFileTypes: true
1875
+ })) {
1706
1876
  const full = path19.join(current, entry.name);
1707
1877
  if (entry.isDirectory()) stack.push(full);
1708
1878
  else if (entry.name.endsWith(".md")) {
@@ -1802,9 +1972,15 @@ function portfolioCommand() {
1802
1972
  ),
1803
1973
  ""
1804
1974
  ];
1805
- const destination = path20.join(vault.root, "portfolio", "Portfolio.md");
1975
+ const destination = path20.join(
1976
+ vault.root,
1977
+ "portfolio",
1978
+ "Portfolio.md"
1979
+ );
1806
1980
  await fs15.outputFile(destination, lines.join("\n"), "utf8");
1807
- spin.succeed(`Portf\xF3lio atualizado com ${items.length} projeto(s)!`);
1981
+ spin.succeed(
1982
+ `Portf\xF3lio atualizado com ${items.length} projeto(s)!`
1983
+ );
1808
1984
  logger.item(path20.relative(vault.root, destination));
1809
1985
  } catch (error) {
1810
1986
  spin.fail("Falha ao gerar o portf\xF3lio.");
@@ -1854,7 +2030,10 @@ async function proximosCompromissos(vault, matriz, dias = 7) {
1854
2030
  compromissos.push({
1855
2031
  tipo: spec.tipo,
1856
2032
  disciplina: disciplina.nome,
1857
- titulo: extractTitle(content, file.replace(/\.md$/, "")),
2033
+ titulo: extractTitle(
2034
+ content,
2035
+ file.replace(/\.md$/, "")
2036
+ ),
1858
2037
  data,
1859
2038
  arquivo: path21.relative(vault.root, full)
1860
2039
  });
@@ -1911,7 +2090,9 @@ function semestreCommand() {
1911
2090
  if (numero === void 0) {
1912
2091
  const atual = getSemestre(matriz, vault.config.semestreAtual);
1913
2092
  logger.title(`${atual.numero}\xBA semestre`);
1914
- logger.info(`Progresso do curso: ${progressBar(atual.numero - 1, 6)}`);
2093
+ logger.info(
2094
+ `Progresso do curso: ${progressBar(atual.numero - 1, 6)}`
2095
+ );
1915
2096
  for (const d of atual.disciplinas) {
1916
2097
  logger.item(`${d.nome} (${d.aulas} aulas)`);
1917
2098
  }
@@ -1940,8 +2121,11 @@ async function collectDates(root) {
1940
2121
  const stack = [root];
1941
2122
  while (stack.length > 0) {
1942
2123
  const current = stack.pop();
1943
- for (const entry of await fs17.readdir(current, { withFileTypes: true })) {
1944
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2124
+ for (const entry of await fs17.readdir(current, {
2125
+ withFileTypes: true
2126
+ })) {
2127
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
2128
+ continue;
1945
2129
  const full = path22.join(current, entry.name);
1946
2130
  if (entry.isDirectory()) stack.push(full);
1947
2131
  else {
@@ -1997,7 +2181,9 @@ function statsCommand() {
1997
2181
  return;
1998
2182
  }
1999
2183
  logger.title("Suas estat\xEDsticas de estudo");
2000
- logger.info(`\u{1F525} Streak: ${chalk4.bold(stats.streak)} dia(s) seguidos`);
2184
+ logger.info(
2185
+ `\u{1F525} Streak: ${chalk4.bold(stats.streak)} dia(s) seguidos`
2186
+ );
2001
2187
  logger.info(`\u23F1\uFE0F Horas estudadas: ${stats.horasEstudadas}h`);
2002
2188
  logger.info(`\u{1F4DD} Notas criadas: ${stats.totalNotas}`);
2003
2189
  logger.blank();