minerva-cli 0.1.0 → 0.2.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.
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.0";
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,98 @@ ${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
+ return {
1234
+ name: `ollama (${model} @ ${baseUrl})`,
1235
+ async generate(prompt, material) {
1236
+ let response;
1237
+ try {
1238
+ response = await fetch(`${baseUrl}/api/chat`, {
1239
+ method: "POST",
1240
+ headers,
1241
+ body: JSON.stringify({
1242
+ model,
1243
+ stream: false,
1244
+ messages: [
1245
+ { role: "system", content: SYSTEM_PROMPT },
1246
+ {
1247
+ role: "user",
1248
+ content: `${prompt}
1249
+
1250
+ <material>
1251
+ ${material}
1252
+ </material>`
1253
+ }
1254
+ ]
1255
+ })
1256
+ });
1257
+ } catch (cause) {
1258
+ throw new Error(
1259
+ `N\xE3o consegui falar com o Ollama em ${baseUrl}.
1260
+ Local: instale com "curl -fsSL https://ollama.com/install.sh | sh" e rode "ollama serve".
1261
+ VPS: defina OLLAMA_URL apontando para o seu servidor.`,
1262
+ { cause }
1263
+ );
1264
+ }
1265
+ if (response.status === 404) {
1266
+ throw new Error(
1267
+ `O modelo "${model}" n\xE3o est\xE1 instalado no Ollama. Rode: ollama pull ${model}`
1268
+ );
1269
+ }
1270
+ if (!response.ok) {
1271
+ throw new Error(
1272
+ `Ollama respondeu HTTP ${response.status}: ${await response.text()}`
1273
+ );
1274
+ }
1275
+ const data = await response.json();
1276
+ const content = data.message?.content;
1277
+ if (!content) {
1278
+ throw new Error("O Ollama retornou uma resposta vazia.");
1279
+ }
1280
+ return content;
1281
+ }
1282
+ };
1283
+ }
1284
+ async function ollamaDisponivel() {
1285
+ const baseUrl = (process.env["OLLAMA_URL"] ?? OLLAMA_DEFAULT_URL).replace(
1286
+ /\/$/,
1287
+ ""
1288
+ );
1289
+ try {
1290
+ const response = await fetch(`${baseUrl}/api/tags`, {
1291
+ signal: AbortSignal.timeout(2e3)
1292
+ });
1293
+ return response.ok;
1294
+ } catch {
1295
+ return false;
1296
+ }
1297
+ }
1298
+ async function resolveProvider() {
1299
+ const forced = process.env["MINERVA_AI"]?.toLowerCase();
1300
+ if (forced === "anthropic") return anthropicProvider();
1301
+ if (forced === "ollama") return ollamaProvider();
1302
+ if (forced) {
1303
+ throw new Error(
1304
+ `Provedor desconhecido em MINERVA_AI: "${forced}". Use "anthropic" ou "ollama".`
1305
+ );
1306
+ }
1307
+ if (process.env["ANTHROPIC_API_KEY"]) return anthropicProvider();
1308
+ if (await ollamaDisponivel()) return ollamaProvider();
1309
+ throw new Error(
1310
+ '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"'
1311
+ );
1312
+ }
1185
1313
  function resumoPrompt(disciplina, tema) {
1186
1314
  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
1315
  }
@@ -1206,7 +1334,9 @@ var DATE_HINT = "Use uma data v\xE1lida no formato YYYY-MM-DD.";
1206
1334
  async function readMaterial(dir) {
1207
1335
  if (!await fs11.pathExists(dir)) return "";
1208
1336
  const parts = [];
1209
- for (const file of (await fs11.readdir(dir)).filter((f) => f.endsWith(".md"))) {
1337
+ for (const file of (await fs11.readdir(dir)).filter(
1338
+ (f) => f.endsWith(".md")
1339
+ )) {
1210
1340
  parts.push(await fs11.readFile(path14.join(dir, file), "utf8"));
1211
1341
  }
1212
1342
  return parts.join("\n\n---\n\n");
@@ -1222,7 +1352,7 @@ function noteCommand(spec) {
1222
1352
  if (spec.ai) {
1223
1353
  command.option(
1224
1354
  "--ai",
1225
- `gera o conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/ (requer ANTHROPIC_API_KEY)`
1355
+ `gera o conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/ (Anthropic ou Ollama)`
1226
1356
  );
1227
1357
  }
1228
1358
  command.action(
@@ -1256,13 +1386,13 @@ function noteCommand(spec) {
1256
1386
  }
1257
1387
  let conteudoIA;
1258
1388
  if (spec.ai && options["ai"]) {
1259
- const provider = anthropicProvider();
1389
+ const provider = await resolveProvider();
1260
1390
  const sourceDir = path14.join(
1261
1391
  disciplinaDir(vault.root, semestre, disciplina),
1262
1392
  spec.ai.sourceFolder
1263
1393
  );
1264
1394
  const spinAi = spinner(
1265
- `Gerando conte\xFAdo com IA (${spec.ai.sourceFolder}/)...`
1395
+ `Gerando conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/...`
1266
1396
  ).start();
1267
1397
  try {
1268
1398
  const material = await readMaterial(sourceDir);
@@ -1286,11 +1416,14 @@ function noteCommand(spec) {
1286
1416
  extra
1287
1417
  });
1288
1418
  if (conteudoIA) {
1289
- await fs11.appendFile(file, `
1419
+ await fs11.appendFile(
1420
+ file,
1421
+ `
1290
1422
  ## Gerado por IA
1291
1423
 
1292
1424
  ${conteudoIA}
1293
- `);
1425
+ `
1426
+ );
1294
1427
  }
1295
1428
  spin.succeed(`Pronto! Nota criada em:`);
1296
1429
  logger.item(path14.relative(vault.root, file));
@@ -1384,14 +1517,18 @@ function flashcardsCommand() {
1384
1517
  );
1385
1518
  const create = flashcardsNewCommand();
1386
1519
  command.addCommand(create, { isDefault: true });
1387
- command.command("export").description("Exporta todos os flashcards para um TSV import\xE1vel no Anki").action(async () => {
1520
+ command.command("export").description(
1521
+ "Exporta todos os flashcards para um TSV import\xE1vel no Anki"
1522
+ ).action(async () => {
1388
1523
  const vault = await requireVault();
1389
1524
  const spin = spinner("Coletando flashcards do vault...").start();
1390
1525
  try {
1391
1526
  const { file, total } = await exportAnki(vault);
1392
1527
  spin.succeed(`${total} flashcard(s) exportado(s)!`);
1393
1528
  logger.item(path15.relative(vault.root, file));
1394
- logger.info("No Anki: Arquivo \u2192 Importar e selecione esse arquivo.");
1529
+ logger.info(
1530
+ "No Anki: Arquivo \u2192 Importar e selecione esse arquivo."
1531
+ );
1395
1532
  } catch (error) {
1396
1533
  spin.fail("Falha na exporta\xE7\xE3o.");
1397
1534
  throw error;
@@ -1442,7 +1579,10 @@ function initCommand() {
1442
1579
  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
1580
  async (pasta, options) => {
1444
1581
  logger.title("Minerva CLI \u2014 vamos montar sua gradua\xE7\xE3o!");
1445
- const aluno = options.aluno ?? await input3({ message: "Seu nome:", default: "Estudante" });
1582
+ const aluno = options.aluno ?? await input3({
1583
+ message: "Seu nome:",
1584
+ default: "Estudante"
1585
+ });
1446
1586
  const semestreAtual = options.semestre !== void 0 ? Number(options.semestre) : await number2({
1447
1587
  message: "Em qual semestre voc\xEA est\xE1? (1-6)",
1448
1588
  default: 1,
@@ -1467,7 +1607,10 @@ function initCommand() {
1467
1607
  spin.text = "Copiando templates...";
1468
1608
  await scaffoldTemplates(root);
1469
1609
  spin.text = "Criando semestres e disciplinas...";
1470
- const totalDisciplinas = await scaffoldSemestres(root, matrizOficial);
1610
+ const totalDisciplinas = await scaffoldSemestres(
1611
+ root,
1612
+ matrizOficial
1613
+ );
1471
1614
  spin.text = "Preparando vault do Obsidian...";
1472
1615
  await scaffoldObsidian(root);
1473
1616
  spin.text = "Gerando README e dashboard...";
@@ -1480,7 +1623,10 @@ function initCommand() {
1480
1623
  semestres: matrizOficial.semestres
1481
1624
  })
1482
1625
  );
1483
- await writeIfMissing(path16.join(root, ".gitignore"), GITIGNORE);
1626
+ await writeIfMissing(
1627
+ path16.join(root, ".gitignore"),
1628
+ GITIGNORE
1629
+ );
1484
1630
  await writeIfMissing(
1485
1631
  path16.join(root, "roadmap", "Roadmap.md"),
1486
1632
  ROADMAP
@@ -1491,14 +1637,20 @@ function initCommand() {
1491
1637
  spin.succeed("Vault criado com sucesso!");
1492
1638
  logger.blank();
1493
1639
  logger.success(`\u{1F4C1} ${root}`);
1494
- logger.item(`${totalDisciplinas} disciplinas em 6 semestres`);
1640
+ logger.item(
1641
+ `${totalDisciplinas} disciplinas em 6 semestres`
1642
+ );
1495
1643
  logger.item("Dashboard em dashboard/Dashboard.md");
1496
1644
  logger.item("Abra a pasta como vault no Obsidian \u{1F7E3}");
1497
1645
  if (!gitOk) {
1498
- logger.warn("Git n\xE3o encontrado \u2014 reposit\xF3rio n\xE3o inicializado.");
1646
+ logger.warn(
1647
+ "Git n\xE3o encontrado \u2014 reposit\xF3rio n\xE3o inicializado."
1648
+ );
1499
1649
  }
1500
1650
  logger.blank();
1501
- logger.info(`Pr\xF3ximo passo: cd ${pasta} && minerva dashboard`);
1651
+ logger.info(
1652
+ `Pr\xF3ximo passo: cd ${pasta} && minerva dashboard`
1653
+ );
1502
1654
  } catch (error) {
1503
1655
  spin.fail("Falha ao criar o vault.");
1504
1656
  throw error;
@@ -1593,7 +1745,9 @@ function notaCommand() {
1593
1745
  `${linha.disciplina}: m\xE9dia ${chalk2.bold(linha.media)} \u2014 ${color(linha.situacao)}`
1594
1746
  );
1595
1747
  for (const nota of linha.notas) {
1596
- logger.item(`${nota.avaliacao}: ${nota.valor} (peso ${nota.peso})`);
1748
+ logger.item(
1749
+ `${nota.avaliacao}: ${nota.valor} (peso ${nota.peso})`
1750
+ );
1597
1751
  }
1598
1752
  }
1599
1753
  });
@@ -1627,7 +1781,9 @@ function notaCommand() {
1627
1781
  peso: Number(options.peso)
1628
1782
  });
1629
1783
  const notas = await loadNotas(vault.root);
1630
- const linha = boletim(notas).find((b) => b.disciplina === disciplina);
1784
+ const linha = boletim(notas).find(
1785
+ (b) => b.disciplina === disciplina
1786
+ );
1631
1787
  logger.success(
1632
1788
  `${avaliacao} = ${valorFinal} registrada em ${disciplina}.`
1633
1789
  );
@@ -1702,7 +1858,9 @@ async function listPendencias(vault, matriz) {
1702
1858
  const stack = [dir];
1703
1859
  while (stack.length > 0) {
1704
1860
  const current = stack.pop();
1705
- for (const entry of await fs14.readdir(current, { withFileTypes: true })) {
1861
+ for (const entry of await fs14.readdir(current, {
1862
+ withFileTypes: true
1863
+ })) {
1706
1864
  const full = path19.join(current, entry.name);
1707
1865
  if (entry.isDirectory()) stack.push(full);
1708
1866
  else if (entry.name.endsWith(".md")) {
@@ -1802,9 +1960,15 @@ function portfolioCommand() {
1802
1960
  ),
1803
1961
  ""
1804
1962
  ];
1805
- const destination = path20.join(vault.root, "portfolio", "Portfolio.md");
1963
+ const destination = path20.join(
1964
+ vault.root,
1965
+ "portfolio",
1966
+ "Portfolio.md"
1967
+ );
1806
1968
  await fs15.outputFile(destination, lines.join("\n"), "utf8");
1807
- spin.succeed(`Portf\xF3lio atualizado com ${items.length} projeto(s)!`);
1969
+ spin.succeed(
1970
+ `Portf\xF3lio atualizado com ${items.length} projeto(s)!`
1971
+ );
1808
1972
  logger.item(path20.relative(vault.root, destination));
1809
1973
  } catch (error) {
1810
1974
  spin.fail("Falha ao gerar o portf\xF3lio.");
@@ -1854,7 +2018,10 @@ async function proximosCompromissos(vault, matriz, dias = 7) {
1854
2018
  compromissos.push({
1855
2019
  tipo: spec.tipo,
1856
2020
  disciplina: disciplina.nome,
1857
- titulo: extractTitle(content, file.replace(/\.md$/, "")),
2021
+ titulo: extractTitle(
2022
+ content,
2023
+ file.replace(/\.md$/, "")
2024
+ ),
1858
2025
  data,
1859
2026
  arquivo: path21.relative(vault.root, full)
1860
2027
  });
@@ -1911,7 +2078,9 @@ function semestreCommand() {
1911
2078
  if (numero === void 0) {
1912
2079
  const atual = getSemestre(matriz, vault.config.semestreAtual);
1913
2080
  logger.title(`${atual.numero}\xBA semestre`);
1914
- logger.info(`Progresso do curso: ${progressBar(atual.numero - 1, 6)}`);
2081
+ logger.info(
2082
+ `Progresso do curso: ${progressBar(atual.numero - 1, 6)}`
2083
+ );
1915
2084
  for (const d of atual.disciplinas) {
1916
2085
  logger.item(`${d.nome} (${d.aulas} aulas)`);
1917
2086
  }
@@ -1940,8 +2109,11 @@ async function collectDates(root) {
1940
2109
  const stack = [root];
1941
2110
  while (stack.length > 0) {
1942
2111
  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;
2112
+ for (const entry of await fs17.readdir(current, {
2113
+ withFileTypes: true
2114
+ })) {
2115
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
2116
+ continue;
1945
2117
  const full = path22.join(current, entry.name);
1946
2118
  if (entry.isDirectory()) stack.push(full);
1947
2119
  else {
@@ -1997,7 +2169,9 @@ function statsCommand() {
1997
2169
  return;
1998
2170
  }
1999
2171
  logger.title("Suas estat\xEDsticas de estudo");
2000
- logger.info(`\u{1F525} Streak: ${chalk4.bold(stats.streak)} dia(s) seguidos`);
2172
+ logger.info(
2173
+ `\u{1F525} Streak: ${chalk4.bold(stats.streak)} dia(s) seguidos`
2174
+ );
2001
2175
  logger.info(`\u23F1\uFE0F Horas estudadas: ${stats.horasEstudadas}h`);
2002
2176
  logger.info(`\u{1F4DD} Notas criadas: ${stats.totalNotas}`);
2003
2177
  logger.blank();