minerva-cli 0.1.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/index.cjs +2137 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2097 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/templates/aula.hbs +38 -0
- package/templates/cheatsheet.hbs +26 -0
- package/templates/dashboard.hbs +46 -0
- package/templates/disciplina.hbs +42 -0
- package/templates/flashcard.hbs +17 -0
- package/templates/projeto.hbs +40 -0
- package/templates/prova.hbs +36 -0
- package/templates/readme.hbs +51 -0
- package/templates/resumo-final.hbs +31 -0
- package/templates/resumo.hbs +30 -0
- package/templates/trabalho.hbs +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import chalk5 from "chalk";
|
|
5
|
+
|
|
6
|
+
// src/program.ts
|
|
7
|
+
import { Command as Command20 } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/config/constants.ts
|
|
10
|
+
var APP_NAME = "minerva";
|
|
11
|
+
var APP_DESCRIPTION = "Organiza\xE7\xE3o acad\xEAmica para estudantes de ADS da FATEC \u{1F989}";
|
|
12
|
+
var APP_VERSION = "0.1.0";
|
|
13
|
+
var DEFAULT_VAULT_DIRNAME = "fatec-ads";
|
|
14
|
+
var MATRIZ_URL = "https://fatectq.cps.sp.gov.br/matriz-curricular-ads/";
|
|
15
|
+
|
|
16
|
+
// src/commands/cheatsheet.ts
|
|
17
|
+
import path5 from "path";
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
|
|
20
|
+
// src/services/config.ts
|
|
21
|
+
import path2 from "path";
|
|
22
|
+
import fs2 from "fs-extra";
|
|
23
|
+
|
|
24
|
+
// src/types/index.ts
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
var disciplinaSchema = z.object({
|
|
27
|
+
nome: z.string().min(1),
|
|
28
|
+
/** Carga horária em aulas, conforme a matriz oficial. */
|
|
29
|
+
aulas: z.number().int().positive()
|
|
30
|
+
});
|
|
31
|
+
var semestreSchema = z.object({
|
|
32
|
+
numero: z.number().int().min(1).max(6),
|
|
33
|
+
disciplinas: z.array(disciplinaSchema).min(1)
|
|
34
|
+
});
|
|
35
|
+
var matrizSchema = z.object({
|
|
36
|
+
curso: z.string(),
|
|
37
|
+
instituicao: z.string(),
|
|
38
|
+
fonte: z.string().url(),
|
|
39
|
+
atualizadaEm: z.string(),
|
|
40
|
+
semestres: z.array(semestreSchema).length(6)
|
|
41
|
+
});
|
|
42
|
+
var minervaConfigSchema = z.object({
|
|
43
|
+
versao: z.string(),
|
|
44
|
+
aluno: z.string(),
|
|
45
|
+
semestreAtual: z.number().int().min(1).max(6),
|
|
46
|
+
criadoEm: z.string(),
|
|
47
|
+
horasEstudadas: z.number().nonnegative().default(0)
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/utils/paths.ts
|
|
51
|
+
import path from "path";
|
|
52
|
+
import { fileURLToPath } from "url";
|
|
53
|
+
import fs from "fs-extra";
|
|
54
|
+
var CONFIG_FILE = "minerva.json";
|
|
55
|
+
var VAULT_FOLDERS = [
|
|
56
|
+
"dashboard",
|
|
57
|
+
"roadmap",
|
|
58
|
+
"curriculo",
|
|
59
|
+
"portfolio",
|
|
60
|
+
"entrevistas",
|
|
61
|
+
"estagio",
|
|
62
|
+
"certificados",
|
|
63
|
+
"artigos",
|
|
64
|
+
"livros",
|
|
65
|
+
"snippets",
|
|
66
|
+
"flashcards",
|
|
67
|
+
"cheatsheets",
|
|
68
|
+
"laboratorio",
|
|
69
|
+
"desafios",
|
|
70
|
+
"templates",
|
|
71
|
+
"projetos-pessoais"
|
|
72
|
+
];
|
|
73
|
+
var DISCIPLINA_FOLDERS = [
|
|
74
|
+
"Aulas",
|
|
75
|
+
"Projetos",
|
|
76
|
+
"Trabalhos",
|
|
77
|
+
"Exercicios",
|
|
78
|
+
"Resumos",
|
|
79
|
+
"Flashcards",
|
|
80
|
+
"Provas",
|
|
81
|
+
"Laboratorio",
|
|
82
|
+
"Referencias"
|
|
83
|
+
];
|
|
84
|
+
function semestreDir(root, numero) {
|
|
85
|
+
return path.join(root, `${numero}-semestre`);
|
|
86
|
+
}
|
|
87
|
+
function builtinTemplatesDir() {
|
|
88
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
89
|
+
const candidates = [
|
|
90
|
+
path.resolve(here, "../templates"),
|
|
91
|
+
path.resolve(here, "../../templates"),
|
|
92
|
+
path.resolve(here, "../../src/templates")
|
|
93
|
+
];
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
if (fs.existsSync(path.join(candidate, "aula.hbs"))) return candidate;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Templates embutidos n\xE3o encontrados. Reinstale a minerva-cli."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async function findVaultRoot(cwd) {
|
|
102
|
+
let dir = path.resolve(cwd);
|
|
103
|
+
for (; ; ) {
|
|
104
|
+
if (await fs.pathExists(path.join(dir, CONFIG_FILE))) return dir;
|
|
105
|
+
const parent = path.dirname(dir);
|
|
106
|
+
if (parent === dir) return null;
|
|
107
|
+
dir = parent;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/services/config.ts
|
|
112
|
+
async function loadConfig(root) {
|
|
113
|
+
const raw = await fs2.readJson(path2.join(root, CONFIG_FILE));
|
|
114
|
+
return minervaConfigSchema.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
async function saveConfig(root, config) {
|
|
117
|
+
await fs2.writeJson(path2.join(root, CONFIG_FILE), config, { spaces: 4 });
|
|
118
|
+
}
|
|
119
|
+
async function requireVault(cwd = process.cwd()) {
|
|
120
|
+
const root = await findVaultRoot(cwd);
|
|
121
|
+
if (!root) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'Nenhum vault Minerva encontrado. Rode "minerva init" primeiro (ou entre na pasta do seu vault).'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return { root, config: await loadConfig(root) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/services/template.ts
|
|
130
|
+
import path3 from "path";
|
|
131
|
+
import fs3 from "fs-extra";
|
|
132
|
+
import Handlebars from "handlebars";
|
|
133
|
+
|
|
134
|
+
// src/utils/slug.ts
|
|
135
|
+
function slugify(text) {
|
|
136
|
+
return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
137
|
+
}
|
|
138
|
+
function safeFolderName(text) {
|
|
139
|
+
return text.replace(/[\\/:*?"<>|]/g, " ").replace(/\s+/g, " ").trim();
|
|
140
|
+
}
|
|
141
|
+
function todayISO(date = /* @__PURE__ */ new Date()) {
|
|
142
|
+
return date.toISOString().slice(0, 10);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/services/template.ts
|
|
146
|
+
var handlebars = Handlebars.create();
|
|
147
|
+
handlebars.registerHelper("hoje", () => todayISO());
|
|
148
|
+
async function resolveTemplate(name, vaultRoot) {
|
|
149
|
+
const filename = `${name}.hbs`;
|
|
150
|
+
if (vaultRoot) {
|
|
151
|
+
const custom = path3.join(vaultRoot, "templates", filename);
|
|
152
|
+
if (await fs3.pathExists(custom)) return fs3.readFile(custom, "utf8");
|
|
153
|
+
}
|
|
154
|
+
return fs3.readFile(path3.join(builtinTemplatesDir(), filename), "utf8");
|
|
155
|
+
}
|
|
156
|
+
async function renderTemplate(name, data, vaultRoot) {
|
|
157
|
+
const source = await resolveTemplate(name, vaultRoot);
|
|
158
|
+
return handlebars.compile(source)(data);
|
|
159
|
+
}
|
|
160
|
+
async function renderToFile(name, data, destination, vaultRoot) {
|
|
161
|
+
if (await fs3.pathExists(destination)) {
|
|
162
|
+
throw new Error(`Arquivo j\xE1 existe: ${destination}`);
|
|
163
|
+
}
|
|
164
|
+
const content = await renderTemplate(name, data, vaultRoot);
|
|
165
|
+
await fs3.outputFile(destination, content, "utf8");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/utils/logger.ts
|
|
169
|
+
import chalk from "chalk";
|
|
170
|
+
import ora from "ora";
|
|
171
|
+
var logger = {
|
|
172
|
+
info(message) {
|
|
173
|
+
console.log(`${chalk.blue("\u2139")} ${message}`);
|
|
174
|
+
},
|
|
175
|
+
success(message) {
|
|
176
|
+
console.log(`${chalk.green("\u2714")} ${message}`);
|
|
177
|
+
},
|
|
178
|
+
warn(message) {
|
|
179
|
+
console.warn(`${chalk.yellow("\u26A0")} ${message}`);
|
|
180
|
+
},
|
|
181
|
+
error(message) {
|
|
182
|
+
console.error(`${chalk.red("\u2716")} ${chalk.red(message)}`);
|
|
183
|
+
},
|
|
184
|
+
title(message) {
|
|
185
|
+
console.log(`
|
|
186
|
+
${chalk.bold.magenta("\u{1F989} " + message)}
|
|
187
|
+
`);
|
|
188
|
+
},
|
|
189
|
+
item(message) {
|
|
190
|
+
console.log(` ${chalk.dim("\u2022")} ${message}`);
|
|
191
|
+
},
|
|
192
|
+
blank() {
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function spinner(text) {
|
|
197
|
+
return ora({ text, color: "magenta" });
|
|
198
|
+
}
|
|
199
|
+
function progressBar(current, total, width = 20) {
|
|
200
|
+
const ratio = total === 0 ? 0 : Math.min(Math.max(current / total, 0), 1);
|
|
201
|
+
const filled = Math.round(ratio * width);
|
|
202
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
203
|
+
return `[${bar}] ${Math.round(ratio * 100)}%`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/utils/prompts.ts
|
|
207
|
+
import { input, number, select } from "@inquirer/prompts";
|
|
208
|
+
|
|
209
|
+
// src/services/matriz.ts
|
|
210
|
+
import path4 from "path";
|
|
211
|
+
import fs4 from "fs-extra";
|
|
212
|
+
|
|
213
|
+
// src/data/matriz.ts
|
|
214
|
+
var matrizOficial = {
|
|
215
|
+
curso: "An\xE1lise e Desenvolvimento de Sistemas",
|
|
216
|
+
instituicao: "FATEC Taquaritinga",
|
|
217
|
+
fonte: "https://fatectq.cps.sp.gov.br/matriz-curricular-ads/",
|
|
218
|
+
atualizadaEm: "2026-07-13",
|
|
219
|
+
semestres: [
|
|
220
|
+
{
|
|
221
|
+
numero: 1,
|
|
222
|
+
disciplinas: [
|
|
223
|
+
{ nome: "Programa\xE7\xE3o em Microinform\xE1tica", aulas: 80 },
|
|
224
|
+
{ nome: "Sistemas de Informa\xE7\xE3o", aulas: 80 },
|
|
225
|
+
{ nome: "Algoritmos e L\xF3gica de Programa\xE7\xE3o", aulas: 80 },
|
|
226
|
+
{
|
|
227
|
+
nome: "Arquitetura e Organiza\xE7\xE3o de Computadores",
|
|
228
|
+
aulas: 80
|
|
229
|
+
},
|
|
230
|
+
{ nome: "Administra\xE7\xE3o Geral", aulas: 80 },
|
|
231
|
+
{ nome: "Matem\xE1tica Discreta", aulas: 80 },
|
|
232
|
+
{ nome: "Comunica\xE7\xE3o e Express\xE3o", aulas: 80 },
|
|
233
|
+
{ nome: "Ingl\xEAs I", aulas: 40 }
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
numero: 2,
|
|
238
|
+
disciplinas: [
|
|
239
|
+
{ nome: "Engenharia de Software I", aulas: 80 },
|
|
240
|
+
{ nome: "Linguagem de Programa\xE7\xE3o", aulas: 80 },
|
|
241
|
+
{ nome: "Programa\xE7\xE3o de Scripts", aulas: 80 },
|
|
242
|
+
{ nome: "Sistemas Operacionais I", aulas: 80 },
|
|
243
|
+
{ nome: "Laborat\xF3rio de Hardware", aulas: 40 },
|
|
244
|
+
{ nome: "Contabilidade", aulas: 40 },
|
|
245
|
+
{ nome: "Estat\xEDstica Aplicada", aulas: 80 },
|
|
246
|
+
{ nome: "C\xE1lculo", aulas: 80 },
|
|
247
|
+
{ nome: "Ingl\xEAs II", aulas: 40 }
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
numero: 3,
|
|
252
|
+
disciplinas: [
|
|
253
|
+
{ nome: "Engenharia de Software II", aulas: 80 },
|
|
254
|
+
{ nome: "Intera\xE7\xE3o Humano Computador", aulas: 40 },
|
|
255
|
+
{ nome: "Programa\xE7\xE3o Web", aulas: 80 },
|
|
256
|
+
{ nome: "Sistemas Operacionais II", aulas: 80 },
|
|
257
|
+
{ nome: "Estruturas de Dados", aulas: 80 },
|
|
258
|
+
{ nome: "Banco de Dados", aulas: 80 },
|
|
259
|
+
{ nome: "Economia e Finan\xE7as", aulas: 40 },
|
|
260
|
+
{ nome: "Programa\xE7\xE3o Linear e Aplica\xE7\xF5es", aulas: 80 },
|
|
261
|
+
{ nome: "Ingl\xEAs III", aulas: 40 }
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
numero: 4,
|
|
266
|
+
disciplinas: [
|
|
267
|
+
{ nome: "Engenharia de Software III", aulas: 80 },
|
|
268
|
+
{ nome: "Programa\xE7\xE3o Orientada a Objetos", aulas: 80 },
|
|
269
|
+
{ nome: "Redes de Computadores", aulas: 80 },
|
|
270
|
+
{ nome: "Seguran\xE7a da Informa\xE7\xE3o", aulas: 40 },
|
|
271
|
+
{ nome: "Programa\xE7\xE3o para Banco de Dados", aulas: 80 },
|
|
272
|
+
{ nome: "Laborat\xF3rio de Banco de Dados", aulas: 80 },
|
|
273
|
+
{ nome: "Gest\xE3o de Projetos", aulas: 80 },
|
|
274
|
+
{ nome: "Sociedade e Tecnologia", aulas: 40 },
|
|
275
|
+
{ nome: "Ingl\xEAs IV", aulas: 40 }
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
numero: 5,
|
|
280
|
+
disciplinas: [
|
|
281
|
+
{ nome: "Laborat\xF3rio de Engenharia de Software", aulas: 80 },
|
|
282
|
+
{ nome: "Gest\xE3o de Equipes", aulas: 40 },
|
|
283
|
+
{ nome: "T\xF3picos Especiais em Inform\xE1tica", aulas: 80 },
|
|
284
|
+
{ nome: "Empreendedorismo", aulas: 40 },
|
|
285
|
+
{
|
|
286
|
+
nome: "Metodologia da Pesquisa Cient\xEDfico-Tecnol\xF3gica",
|
|
287
|
+
aulas: 40
|
|
288
|
+
},
|
|
289
|
+
{ nome: "Ingl\xEAs V", aulas: 40 },
|
|
290
|
+
{ nome: "Trabalho de Gradua\xE7\xE3o I", aulas: 80 }
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
numero: 6,
|
|
295
|
+
disciplinas: [
|
|
296
|
+
{ nome: "\xC9tica e Responsabilidade Profissional", aulas: 40 },
|
|
297
|
+
{ nome: "Auditoria de Sistemas", aulas: 80 },
|
|
298
|
+
{
|
|
299
|
+
nome: "Gest\xE3o e Governan\xE7a de Tecnologia da Informa\xE7\xE3o",
|
|
300
|
+
aulas: 80
|
|
301
|
+
},
|
|
302
|
+
{ nome: "Ingl\xEAs VI", aulas: 40 },
|
|
303
|
+
{ nome: "Trabalho de Gradua\xE7\xE3o II", aulas: 80 },
|
|
304
|
+
{ nome: "Est\xE1gio Supervisionado", aulas: 240 }
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/services/matriz.ts
|
|
311
|
+
var MATRIZ_FILE = path4.join("curriculo", "matriz.json");
|
|
312
|
+
async function loadMatriz(vaultRoot) {
|
|
313
|
+
if (vaultRoot) {
|
|
314
|
+
const file = path4.join(vaultRoot, MATRIZ_FILE);
|
|
315
|
+
if (await fs4.pathExists(file)) {
|
|
316
|
+
const raw = await fs4.readJson(file);
|
|
317
|
+
return matrizSchema.parse(raw);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return matrizOficial;
|
|
321
|
+
}
|
|
322
|
+
async function saveMatriz(vaultRoot, matriz) {
|
|
323
|
+
await fs4.outputJson(path4.join(vaultRoot, MATRIZ_FILE), matriz, {
|
|
324
|
+
spaces: 4
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function getSemestre(matriz, numero) {
|
|
328
|
+
const semestre = matriz.semestres.find((s) => s.numero === numero);
|
|
329
|
+
if (!semestre) {
|
|
330
|
+
throw new Error(`Semestre inv\xE1lido: ${numero}. Use um valor de 1 a 6.`);
|
|
331
|
+
}
|
|
332
|
+
return semestre;
|
|
333
|
+
}
|
|
334
|
+
function parseMatrizHtml(html, fonte) {
|
|
335
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, "\n").replace(/ /g, " ").replace(/&/g, "&").replace(/–|–/g, "\u2013");
|
|
336
|
+
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
337
|
+
const semestres = [];
|
|
338
|
+
let atual = null;
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
const header = /^(\d)\s*[ºo°]?\s*semestre/i.exec(line);
|
|
341
|
+
if (header) {
|
|
342
|
+
const numero = Number(header[1]);
|
|
343
|
+
if (numero >= 1 && numero <= 6) {
|
|
344
|
+
atual = { numero, disciplinas: [] };
|
|
345
|
+
semestres.push(atual);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const item = /^(.+?)\s*[–—-]\s*(\d+)\s*aulas?/i.exec(line);
|
|
350
|
+
if (item && atual) {
|
|
351
|
+
atual.disciplinas.push({
|
|
352
|
+
nome: item[1].trim(),
|
|
353
|
+
aulas: Number(item[2])
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return matrizSchema.parse({
|
|
358
|
+
curso: matrizOficial.curso,
|
|
359
|
+
instituicao: matrizOficial.instituicao,
|
|
360
|
+
fonte,
|
|
361
|
+
atualizadaEm: todayISO(),
|
|
362
|
+
semestres
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async function fetchMatriz(url = matrizOficial.fonte) {
|
|
366
|
+
const response = await fetch(url);
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`Falha ao baixar a matriz (HTTP ${response.status}).`);
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
return parseMatrizHtml(await response.text(), url);
|
|
372
|
+
} catch (cause) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
"N\xE3o consegui interpretar a p\xE1gina da FATEC (o layout pode ter mudado). A matriz embutida na CLI continua v\xE1lida.",
|
|
375
|
+
{ cause }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/utils/prompts.ts
|
|
381
|
+
async function pickDisciplina(matriz, semestre, preset) {
|
|
382
|
+
const disciplinas = getSemestre(matriz, semestre).disciplinas;
|
|
383
|
+
if (preset) {
|
|
384
|
+
const found = disciplinas.find(
|
|
385
|
+
(d) => d.nome.toLowerCase() === preset.toLowerCase()
|
|
386
|
+
);
|
|
387
|
+
if (found) return found.nome;
|
|
388
|
+
}
|
|
389
|
+
return select({
|
|
390
|
+
message: `Disciplina (${semestre}\xBA semestre):`,
|
|
391
|
+
choices: disciplinas.map((d) => ({ value: d.nome }))
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async function pickSemestre(defaultValue) {
|
|
395
|
+
const value = await number({
|
|
396
|
+
message: "Semestre (1-6):",
|
|
397
|
+
default: defaultValue,
|
|
398
|
+
min: 1,
|
|
399
|
+
max: 6
|
|
400
|
+
});
|
|
401
|
+
return value ?? defaultValue;
|
|
402
|
+
}
|
|
403
|
+
async function askTitulo(message) {
|
|
404
|
+
return input({
|
|
405
|
+
message,
|
|
406
|
+
validate: (value) => value.trim().length > 0 || "O t\xEDtulo n\xE3o pode ficar vazio."
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/commands/cheatsheet.ts
|
|
411
|
+
function cheatsheetCommand() {
|
|
412
|
+
return new Command("cheatsheet").description("Cria uma cheatsheet avulsa (ex.: Git, SQL, Linux)").argument("[titulo]", "assunto da cheatsheet").action(async (titulo) => {
|
|
413
|
+
const vault = await requireVault();
|
|
414
|
+
const tituloFinal = titulo ?? await askTitulo("Assunto da cheatsheet:");
|
|
415
|
+
const destination = path5.join(
|
|
416
|
+
vault.root,
|
|
417
|
+
"cheatsheets",
|
|
418
|
+
`${slugify(tituloFinal)}.md`
|
|
419
|
+
);
|
|
420
|
+
const spin = spinner("Criando cheatsheet...").start();
|
|
421
|
+
try {
|
|
422
|
+
await renderToFile(
|
|
423
|
+
"cheatsheet",
|
|
424
|
+
{ titulo: tituloFinal, disciplina: "" },
|
|
425
|
+
destination,
|
|
426
|
+
vault.root
|
|
427
|
+
);
|
|
428
|
+
spin.succeed("Cheatsheet criada!");
|
|
429
|
+
logger.item(path5.relative(vault.root, destination));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
spin.fail("N\xE3o consegui criar a cheatsheet.");
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/commands/commit.ts
|
|
438
|
+
import { Command as Command2 } from "commander";
|
|
439
|
+
|
|
440
|
+
// src/services/git.ts
|
|
441
|
+
import { execFile } from "child_process";
|
|
442
|
+
import { promisify } from "util";
|
|
443
|
+
var exec = promisify(execFile);
|
|
444
|
+
async function isGitRepo(cwd) {
|
|
445
|
+
try {
|
|
446
|
+
await exec("git", ["rev-parse", "--git-dir"], { cwd });
|
|
447
|
+
return true;
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function commitVault(root, message) {
|
|
453
|
+
if (!await isGitRepo(root)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
'O vault n\xE3o \xE9 um reposit\xF3rio Git. Rode "git init" dentro dele.'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
await exec("git", ["add", "-A"], { cwd: root });
|
|
459
|
+
try {
|
|
460
|
+
await exec("git", ["commit", "-m", message], { cwd: root });
|
|
461
|
+
return true;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const stdout = error instanceof Error && "stdout" in error ? String(error.stdout) : "";
|
|
464
|
+
if (stdout.includes("nothing to commit")) return false;
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/commands/commit.ts
|
|
470
|
+
function commitCommand() {
|
|
471
|
+
return new Command2("commit").description("Faz git add + commit de todas as mudan\xE7as do vault").argument("[mensagem]", "mensagem do commit").action(async (mensagem) => {
|
|
472
|
+
const vault = await requireVault();
|
|
473
|
+
const spin = spinner("Commitando o vault...").start();
|
|
474
|
+
try {
|
|
475
|
+
const committed = await commitVault(
|
|
476
|
+
vault.root,
|
|
477
|
+
mensagem ?? `minerva: estudos de ${todayISO()}`
|
|
478
|
+
);
|
|
479
|
+
if (committed) spin.succeed("Commit criado!");
|
|
480
|
+
else spin.info("Nada novo para commitar.");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
spin.fail("Falha ao commitar.");
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/commands/completion.ts
|
|
489
|
+
import { Command as Command3 } from "commander";
|
|
490
|
+
function buildScript(shell, commands) {
|
|
491
|
+
const list = commands.join(" ");
|
|
492
|
+
if (shell === "bash") {
|
|
493
|
+
return `# minerva \u2014 bash completion (adicione ao ~/.bashrc)
|
|
494
|
+
_minerva_completions() {
|
|
495
|
+
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
496
|
+
COMPREPLY=($(compgen -W "${list}" -- "\${COMP_WORDS[1]}"))
|
|
497
|
+
fi
|
|
498
|
+
}
|
|
499
|
+
complete -F _minerva_completions minerva
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
return `# minerva \u2014 zsh completion (adicione ao ~/.zshrc)
|
|
503
|
+
_minerva() {
|
|
504
|
+
if (( CURRENT == 2 )); then
|
|
505
|
+
compadd ${list}
|
|
506
|
+
fi
|
|
507
|
+
}
|
|
508
|
+
compdef _minerva minerva
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
function completionCommand() {
|
|
512
|
+
return new Command3("completion").description("Gera script de autocomplete para bash ou zsh").argument("<shell>", "bash ou zsh").action(function(shell) {
|
|
513
|
+
if (shell !== "bash" && shell !== "zsh") {
|
|
514
|
+
throw new Error('Shell n\xE3o suportado. Use "bash" ou "zsh".');
|
|
515
|
+
}
|
|
516
|
+
const commands = (this.parent?.commands ?? []).map((c) => c.name()).filter((name) => name !== "completion");
|
|
517
|
+
console.log(buildScript(shell, commands));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/commands/config.ts
|
|
522
|
+
import { Command as Command4 } from "commander";
|
|
523
|
+
var EDITABLE_KEYS = ["aluno", "semestreAtual", "horasEstudadas"];
|
|
524
|
+
function configCommand() {
|
|
525
|
+
const command = new Command4("config").description(
|
|
526
|
+
"Mostra ou edita a configura\xE7\xE3o do vault (minerva.json)"
|
|
527
|
+
);
|
|
528
|
+
command.command("show", { isDefault: true }).description("Mostra a configura\xE7\xE3o atual").option("--json", "sa\xEDda em JSON").action(async (options) => {
|
|
529
|
+
const vault = await requireVault();
|
|
530
|
+
if (options.json) {
|
|
531
|
+
console.log(JSON.stringify(vault.config, null, 2));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
logger.title("Configura\xE7\xE3o do vault");
|
|
535
|
+
for (const [key, value] of Object.entries(vault.config)) {
|
|
536
|
+
logger.item(`${key}: ${String(value)}`);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
command.command("set").description(`Edita uma chave (${EDITABLE_KEYS.join(", ")})`).argument("<chave>").argument("<valor>").action(async (chave, valor) => {
|
|
540
|
+
const vault = await requireVault();
|
|
541
|
+
if (!EDITABLE_KEYS.includes(chave)) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Chave inv\xE1lida: "${chave}". Chaves edit\xE1veis: ${EDITABLE_KEYS.join(", ")}.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const parsed = chave === "aluno" ? valor : Number(valor.replace(",", "."));
|
|
547
|
+
const config = minervaConfigSchema.parse({
|
|
548
|
+
...vault.config,
|
|
549
|
+
[chave]: parsed
|
|
550
|
+
});
|
|
551
|
+
await saveConfig(vault.root, config);
|
|
552
|
+
logger.success(`${chave} = ${String(parsed)}`);
|
|
553
|
+
});
|
|
554
|
+
return command;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/commands/dashboard.ts
|
|
558
|
+
import path8 from "path";
|
|
559
|
+
import { Command as Command5 } from "commander";
|
|
560
|
+
|
|
561
|
+
// src/services/dashboard.ts
|
|
562
|
+
import path7 from "path";
|
|
563
|
+
import fs6 from "fs-extra";
|
|
564
|
+
|
|
565
|
+
// src/services/scaffold.ts
|
|
566
|
+
import path6 from "path";
|
|
567
|
+
import fs5 from "fs-extra";
|
|
568
|
+
function disciplinaDir(root, semestre, disciplina) {
|
|
569
|
+
return path6.join(semestreDir(root, semestre), safeFolderName(disciplina));
|
|
570
|
+
}
|
|
571
|
+
async function scaffoldDisciplina(root, semestre, disciplina) {
|
|
572
|
+
const dir = disciplinaDir(root, semestre, disciplina.nome);
|
|
573
|
+
await Promise.all(
|
|
574
|
+
DISCIPLINA_FOLDERS.map((folder) => fs5.ensureDir(path6.join(dir, folder)))
|
|
575
|
+
);
|
|
576
|
+
const files = [
|
|
577
|
+
[
|
|
578
|
+
"README.md",
|
|
579
|
+
() => renderTemplate(
|
|
580
|
+
"disciplina",
|
|
581
|
+
{ disciplina: disciplina.nome, semestre, aulas: disciplina.aulas },
|
|
582
|
+
root
|
|
583
|
+
)
|
|
584
|
+
],
|
|
585
|
+
[
|
|
586
|
+
"Cheatsheet.md",
|
|
587
|
+
() => renderTemplate(
|
|
588
|
+
"cheatsheet",
|
|
589
|
+
{ titulo: disciplina.nome, disciplina: disciplina.nome },
|
|
590
|
+
root
|
|
591
|
+
)
|
|
592
|
+
],
|
|
593
|
+
[
|
|
594
|
+
"Resumo-Final.md",
|
|
595
|
+
() => renderTemplate(
|
|
596
|
+
"resumo-final",
|
|
597
|
+
{ disciplina: disciplina.nome, semestre },
|
|
598
|
+
root
|
|
599
|
+
)
|
|
600
|
+
]
|
|
601
|
+
];
|
|
602
|
+
for (const [filename, render] of files) {
|
|
603
|
+
const destination = path6.join(dir, filename);
|
|
604
|
+
if (!await fs5.pathExists(destination)) {
|
|
605
|
+
await fs5.outputFile(destination, await render(), "utf8");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return dir;
|
|
609
|
+
}
|
|
610
|
+
async function scaffoldVaultFolders(root) {
|
|
611
|
+
await Promise.all(
|
|
612
|
+
VAULT_FOLDERS.map((folder) => fs5.ensureDir(path6.join(root, folder)))
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
async function scaffoldTemplates(root) {
|
|
616
|
+
await fs5.copy(builtinTemplatesDir(), path6.join(root, "templates"), {
|
|
617
|
+
overwrite: false
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async function scaffoldSemestres(root, matriz) {
|
|
621
|
+
let total = 0;
|
|
622
|
+
for (const semestre of matriz.semestres) {
|
|
623
|
+
for (const disciplina of semestre.disciplinas) {
|
|
624
|
+
await scaffoldDisciplina(root, semestre.numero, disciplina);
|
|
625
|
+
total += 1;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return total;
|
|
629
|
+
}
|
|
630
|
+
async function scaffoldObsidian(root) {
|
|
631
|
+
const obsidianDirs = [
|
|
632
|
+
"obsidian/daily-notes",
|
|
633
|
+
"obsidian/canvas",
|
|
634
|
+
"obsidian/attachments",
|
|
635
|
+
"obsidian/excalidraw"
|
|
636
|
+
];
|
|
637
|
+
await Promise.all(
|
|
638
|
+
obsidianDirs.map((dir) => fs5.ensureDir(path6.join(root, dir)))
|
|
639
|
+
);
|
|
640
|
+
const settings = path6.join(root, ".obsidian");
|
|
641
|
+
await fs5.ensureDir(settings);
|
|
642
|
+
await fs5.writeJson(
|
|
643
|
+
path6.join(settings, "app.json"),
|
|
644
|
+
{ attachmentFolderPath: "obsidian/attachments" },
|
|
645
|
+
{ spaces: 4 }
|
|
646
|
+
);
|
|
647
|
+
await fs5.writeJson(
|
|
648
|
+
path6.join(settings, "daily-notes.json"),
|
|
649
|
+
{ folder: "obsidian/daily-notes", format: "YYYY-MM-DD" },
|
|
650
|
+
{ spaces: 4 }
|
|
651
|
+
);
|
|
652
|
+
await fs5.writeJson(
|
|
653
|
+
path6.join(settings, "templates.json"),
|
|
654
|
+
{ folder: "templates" },
|
|
655
|
+
{ spaces: 4 }
|
|
656
|
+
);
|
|
657
|
+
await fs5.writeJson(
|
|
658
|
+
path6.join(settings, "types.json"),
|
|
659
|
+
{
|
|
660
|
+
types: {
|
|
661
|
+
disciplina: "text",
|
|
662
|
+
semestre: "number",
|
|
663
|
+
tipo: "text",
|
|
664
|
+
status: "text",
|
|
665
|
+
data: "date"
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
{ spaces: 4 }
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/services/dashboard.ts
|
|
673
|
+
async function listMarkdown(dir) {
|
|
674
|
+
if (!await fs6.pathExists(dir)) return [];
|
|
675
|
+
const entries = await fs6.readdir(dir);
|
|
676
|
+
return entries.filter((entry) => entry.endsWith(".md"));
|
|
677
|
+
}
|
|
678
|
+
async function countPendencias(dir) {
|
|
679
|
+
if (!await fs6.pathExists(dir)) return 0;
|
|
680
|
+
let count = 0;
|
|
681
|
+
const stack = [dir];
|
|
682
|
+
while (stack.length > 0) {
|
|
683
|
+
const current = stack.pop();
|
|
684
|
+
for (const entry of await fs6.readdir(current, {
|
|
685
|
+
withFileTypes: true
|
|
686
|
+
})) {
|
|
687
|
+
const full = path7.join(current, entry.name);
|
|
688
|
+
if (entry.isDirectory()) stack.push(full);
|
|
689
|
+
else if (entry.name.endsWith(".md")) {
|
|
690
|
+
const content = await fs6.readFile(full, "utf8");
|
|
691
|
+
count += (content.match(/^\s*[-*] \[ \]/gm) ?? []).length;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return count;
|
|
696
|
+
}
|
|
697
|
+
function extractProvaDate(content) {
|
|
698
|
+
const match = /^data-prova:\s*(\d{4}-\d{2}-\d{2})/m.exec(content);
|
|
699
|
+
return match ? match[1] : null;
|
|
700
|
+
}
|
|
701
|
+
async function collectDashboardData(vault, matriz) {
|
|
702
|
+
const semestre = getSemestre(matriz, vault.config.semestreAtual);
|
|
703
|
+
const disciplinas = [];
|
|
704
|
+
const proximasProvas = [];
|
|
705
|
+
const hoje = todayISO();
|
|
706
|
+
for (const disciplina of semestre.disciplinas) {
|
|
707
|
+
const dir = disciplinaDir(vault.root, semestre.numero, disciplina.nome);
|
|
708
|
+
const [aulas, resumos, provas, trabalhos, projetos, pendencias] = await Promise.all([
|
|
709
|
+
listMarkdown(path7.join(dir, "Aulas")),
|
|
710
|
+
listMarkdown(path7.join(dir, "Resumos")),
|
|
711
|
+
listMarkdown(path7.join(dir, "Provas")),
|
|
712
|
+
listMarkdown(path7.join(dir, "Trabalhos")),
|
|
713
|
+
listMarkdown(path7.join(dir, "Projetos")),
|
|
714
|
+
countPendencias(dir)
|
|
715
|
+
]);
|
|
716
|
+
for (const arquivo of provas) {
|
|
717
|
+
const content = await fs6.readFile(
|
|
718
|
+
path7.join(dir, "Provas", arquivo),
|
|
719
|
+
"utf8"
|
|
720
|
+
);
|
|
721
|
+
const data = extractProvaDate(content);
|
|
722
|
+
if (data && data >= hoje) {
|
|
723
|
+
proximasProvas.push({
|
|
724
|
+
disciplina: disciplina.nome,
|
|
725
|
+
arquivo,
|
|
726
|
+
data
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
disciplinas.push({
|
|
731
|
+
nome: disciplina.nome,
|
|
732
|
+
aulas: aulas.length,
|
|
733
|
+
resumos: resumos.length,
|
|
734
|
+
provas: provas.length,
|
|
735
|
+
trabalhos: trabalhos.length,
|
|
736
|
+
projetos: projetos.length,
|
|
737
|
+
pendencias
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
proximasProvas.sort((a, b) => a.data.localeCompare(b.data));
|
|
741
|
+
return {
|
|
742
|
+
geradoEm: hoje,
|
|
743
|
+
semestreAtual: semestre.numero,
|
|
744
|
+
horasEstudadas: vault.config.horasEstudadas,
|
|
745
|
+
progressoCurso: progressBar(semestre.numero - 1, 6),
|
|
746
|
+
disciplinas,
|
|
747
|
+
proximasProvas,
|
|
748
|
+
totalPendencias: disciplinas.reduce((sum, d) => sum + d.pendencias, 0)
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async function generateDashboard(vault, matriz) {
|
|
752
|
+
const data = await collectDashboardData(vault, matriz);
|
|
753
|
+
const content = await renderTemplate(
|
|
754
|
+
"dashboard",
|
|
755
|
+
data,
|
|
756
|
+
vault.root
|
|
757
|
+
);
|
|
758
|
+
const destination = path7.join(vault.root, "dashboard", "Dashboard.md");
|
|
759
|
+
await fs6.outputFile(destination, content, "utf8");
|
|
760
|
+
return destination;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/commands/dashboard.ts
|
|
764
|
+
function dashboardCommand() {
|
|
765
|
+
return new Command5("dashboard").description("Gera/atualiza o dashboard de estudos").action(async () => {
|
|
766
|
+
const vault = await requireVault();
|
|
767
|
+
const matriz = await loadMatriz(vault.root);
|
|
768
|
+
const spin = spinner("Analisando seu vault...").start();
|
|
769
|
+
try {
|
|
770
|
+
const file = await generateDashboard(vault, matriz);
|
|
771
|
+
spin.succeed("Dashboard atualizado!");
|
|
772
|
+
logger.item(path8.relative(vault.root, file));
|
|
773
|
+
} catch (error) {
|
|
774
|
+
spin.fail("Falha ao gerar o dashboard.");
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/commands/disciplina.ts
|
|
781
|
+
import path9 from "path";
|
|
782
|
+
import { Command as Command6 } from "commander";
|
|
783
|
+
function disciplinaCommand() {
|
|
784
|
+
const command = new Command6("disciplina").description(
|
|
785
|
+
"Lista disciplinas ou adiciona uma disciplina extra"
|
|
786
|
+
);
|
|
787
|
+
command.command("list", { isDefault: true }).description("Lista as disciplinas de todos os semestres").action(async () => {
|
|
788
|
+
const vault = await requireVault();
|
|
789
|
+
const matriz = await loadMatriz(vault.root);
|
|
790
|
+
logger.title(matriz.curso);
|
|
791
|
+
for (const semestre of matriz.semestres) {
|
|
792
|
+
const marker = semestre.numero === vault.config.semestreAtual ? " \u{1F448} atual" : "";
|
|
793
|
+
logger.info(`${semestre.numero}\xBA semestre${marker}`);
|
|
794
|
+
for (const d of semestre.disciplinas) {
|
|
795
|
+
logger.item(`${d.nome} (${d.aulas} aulas)`);
|
|
796
|
+
}
|
|
797
|
+
logger.blank();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
command.command("add").description("Cria a estrutura de uma disciplina extra").argument("[nome]", "nome da disciplina").option("-s, --semestre <n>", "semestre onde criar").option("-a, --aulas <n>", "carga hor\xE1ria em aulas", "80").action(
|
|
801
|
+
async (nome, options) => {
|
|
802
|
+
const vault = await requireVault();
|
|
803
|
+
const nomeFinal = nome ?? await askTitulo("Nome da disciplina:");
|
|
804
|
+
const semestre = options.semestre ? Number(options.semestre) : await pickSemestre(vault.config.semestreAtual);
|
|
805
|
+
const spin = spinner("Criando disciplina...").start();
|
|
806
|
+
try {
|
|
807
|
+
const dir = await scaffoldDisciplina(vault.root, semestre, {
|
|
808
|
+
nome: nomeFinal,
|
|
809
|
+
aulas: Number(options.aulas)
|
|
810
|
+
});
|
|
811
|
+
spin.succeed(`Disciplina "${nomeFinal}" criada!`);
|
|
812
|
+
logger.item(path9.relative(vault.root, dir));
|
|
813
|
+
} catch (error) {
|
|
814
|
+
spin.fail("N\xE3o consegui criar a disciplina.");
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
return command;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/commands/doctor.ts
|
|
823
|
+
import path10 from "path";
|
|
824
|
+
import { Command as Command7 } from "commander";
|
|
825
|
+
import fs7 from "fs-extra";
|
|
826
|
+
function doctorCommand() {
|
|
827
|
+
return new Command7("doctor").description("Verifica a sa\xFAde do ambiente e do vault").action(async () => {
|
|
828
|
+
logger.title("Minerva Doctor");
|
|
829
|
+
const root = await findVaultRoot(process.cwd());
|
|
830
|
+
const checks = [
|
|
831
|
+
{
|
|
832
|
+
label: "Node.js",
|
|
833
|
+
run: () => {
|
|
834
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
835
|
+
if (major < 20) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`v${process.versions.node} \u2014 atualize para a LTS (>= 20)`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return Promise.resolve(`v${process.versions.node}`);
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
label: "Templates embutidos",
|
|
845
|
+
run: () => Promise.resolve(builtinTemplatesDir())
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
label: "Vault Minerva",
|
|
849
|
+
run: () => {
|
|
850
|
+
if (!root) {
|
|
851
|
+
throw new Error('n\xE3o encontrado \u2014 rode "minerva init"');
|
|
852
|
+
}
|
|
853
|
+
return Promise.resolve(root);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
];
|
|
857
|
+
if (root) {
|
|
858
|
+
checks.push(
|
|
859
|
+
{
|
|
860
|
+
label: "Configura\xE7\xE3o (minerva.json)",
|
|
861
|
+
run: async () => {
|
|
862
|
+
const config = await loadConfig(root);
|
|
863
|
+
return `aluno: ${config.aluno}, ${config.semestreAtual}\xBA semestre`;
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
label: "Matriz curricular",
|
|
868
|
+
run: async () => {
|
|
869
|
+
const matriz = await loadMatriz(root);
|
|
870
|
+
const total = matriz.semestres.reduce(
|
|
871
|
+
(sum, s) => sum + s.disciplinas.length,
|
|
872
|
+
0
|
|
873
|
+
);
|
|
874
|
+
return `${total} disciplinas (atualizada em ${matriz.atualizadaEm})`;
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
label: "Reposit\xF3rio Git",
|
|
879
|
+
run: async () => {
|
|
880
|
+
if (!await fs7.pathExists(path10.join(root, ".git"))) {
|
|
881
|
+
throw new Error('ausente \u2014 rode "git init" no vault');
|
|
882
|
+
}
|
|
883
|
+
return "ok";
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
label: "Vault do Obsidian",
|
|
888
|
+
run: async () => {
|
|
889
|
+
if (!await fs7.pathExists(path10.join(root, ".obsidian"))) {
|
|
890
|
+
throw new Error(
|
|
891
|
+
'pasta .obsidian ausente \u2014 rode "minerva init" novamente'
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
return "ok";
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
let failures = 0;
|
|
900
|
+
for (const check of checks) {
|
|
901
|
+
try {
|
|
902
|
+
logger.success(`${check.label}: ${await check.run()}`);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
failures += 1;
|
|
905
|
+
logger.error(
|
|
906
|
+
`${check.label}: ${error instanceof Error ? error.message : String(error)}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
logger.blank();
|
|
911
|
+
if (failures === 0) {
|
|
912
|
+
logger.success("Tudo certo! Bons estudos. \u{1F989}");
|
|
913
|
+
} else {
|
|
914
|
+
logger.warn(`${failures} problema(s) encontrado(s).`);
|
|
915
|
+
process.exitCode = 1;
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/commands/estudo.ts
|
|
921
|
+
import { Command as Command8 } from "commander";
|
|
922
|
+
|
|
923
|
+
// src/services/estudo.ts
|
|
924
|
+
import path11 from "path";
|
|
925
|
+
import fs8 from "fs-extra";
|
|
926
|
+
import { z as z2 } from "zod";
|
|
927
|
+
var ESTUDO_LOG_FILE = path11.join("dashboard", "estudo.json");
|
|
928
|
+
var sessaoSchema = z2.object({
|
|
929
|
+
data: z2.string(),
|
|
930
|
+
horas: z2.number().positive(),
|
|
931
|
+
disciplina: z2.string().optional()
|
|
932
|
+
});
|
|
933
|
+
var logSchema = z2.array(sessaoSchema);
|
|
934
|
+
async function loadEstudoLog(root) {
|
|
935
|
+
const file = path11.join(root, ESTUDO_LOG_FILE);
|
|
936
|
+
if (!await fs8.pathExists(file)) return [];
|
|
937
|
+
const raw = await fs8.readJson(file);
|
|
938
|
+
return logSchema.parse(raw);
|
|
939
|
+
}
|
|
940
|
+
async function registrarEstudo(vault, horas, disciplina) {
|
|
941
|
+
if (!Number.isFinite(horas) || horas <= 0) {
|
|
942
|
+
throw new Error("Informe um n\xFAmero de horas maior que zero (ex.: 1.5).");
|
|
943
|
+
}
|
|
944
|
+
const sessao = {
|
|
945
|
+
data: todayISO(),
|
|
946
|
+
horas,
|
|
947
|
+
...disciplina ? { disciplina } : {}
|
|
948
|
+
};
|
|
949
|
+
const log = await loadEstudoLog(vault.root);
|
|
950
|
+
log.push(sessao);
|
|
951
|
+
await fs8.outputJson(path11.join(vault.root, ESTUDO_LOG_FILE), log, {
|
|
952
|
+
spaces: 4
|
|
953
|
+
});
|
|
954
|
+
vault.config.horasEstudadas = Math.round((vault.config.horasEstudadas + horas) * 100) / 100;
|
|
955
|
+
await saveConfig(vault.root, vault.config);
|
|
956
|
+
return sessao;
|
|
957
|
+
}
|
|
958
|
+
function horasPorDisciplina(log) {
|
|
959
|
+
const totals = /* @__PURE__ */ new Map();
|
|
960
|
+
for (const sessao of log) {
|
|
961
|
+
const key = sessao.disciplina ?? "(geral)";
|
|
962
|
+
totals.set(key, (totals.get(key) ?? 0) + sessao.horas);
|
|
963
|
+
}
|
|
964
|
+
return totals;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/commands/estudo.ts
|
|
968
|
+
function estudoCommand() {
|
|
969
|
+
return new Command8("estudo").description("Registra horas de estudo (alimenta o dashboard)").argument("[horas]", "horas estudadas (ex.: 1.5)").option("-d, --disciplina <nome>", "disciplina estudada").option("--json", "sa\xEDda em JSON").action(
|
|
970
|
+
async (horas, options) => {
|
|
971
|
+
const vault = await requireVault();
|
|
972
|
+
if (horas === void 0) {
|
|
973
|
+
const log = await loadEstudoLog(vault.root);
|
|
974
|
+
const totals = horasPorDisciplina(log);
|
|
975
|
+
if (options.json) {
|
|
976
|
+
console.log(
|
|
977
|
+
JSON.stringify(
|
|
978
|
+
{
|
|
979
|
+
total: vault.config.horasEstudadas,
|
|
980
|
+
porDisciplina: Object.fromEntries(totals)
|
|
981
|
+
},
|
|
982
|
+
null,
|
|
983
|
+
2
|
|
984
|
+
)
|
|
985
|
+
);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
logger.title(`${vault.config.horasEstudadas}h estudadas no total`);
|
|
989
|
+
for (const [disciplina2, total] of totals) {
|
|
990
|
+
logger.item(`${disciplina2}: ${total}h`);
|
|
991
|
+
}
|
|
992
|
+
if (totals.size === 0) {
|
|
993
|
+
logger.info("Registre sua primeira sess\xE3o: minerva estudo 1.5");
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const valor = Number(horas.replace(",", "."));
|
|
998
|
+
const matriz = await loadMatriz(vault.root);
|
|
999
|
+
const disciplina = options.disciplina ? await pickDisciplina(
|
|
1000
|
+
matriz,
|
|
1001
|
+
vault.config.semestreAtual,
|
|
1002
|
+
options.disciplina
|
|
1003
|
+
) : void 0;
|
|
1004
|
+
const sessao = await registrarEstudo(vault, valor, disciplina);
|
|
1005
|
+
await generateDashboard(vault, matriz);
|
|
1006
|
+
logger.success(
|
|
1007
|
+
`+${sessao.horas}h registradas${disciplina ? ` em ${disciplina}` : ""}. Total: ${vault.config.horasEstudadas}h. Dashboard atualizado!`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/commands/flashcards.ts
|
|
1014
|
+
import path15 from "path";
|
|
1015
|
+
import { Command as Command10 } from "commander";
|
|
1016
|
+
|
|
1017
|
+
// src/services/anki.ts
|
|
1018
|
+
import path12 from "path";
|
|
1019
|
+
import fs9 from "fs-extra";
|
|
1020
|
+
function parseFlashcards(content, deck) {
|
|
1021
|
+
const cards = [];
|
|
1022
|
+
for (const line of content.split("\n")) {
|
|
1023
|
+
const trimmed = line.trim();
|
|
1024
|
+
if (trimmed.startsWith("#") || trimmed.startsWith(">")) continue;
|
|
1025
|
+
const match = /^(.+?)\s*::\s*(.+)$/.exec(trimmed);
|
|
1026
|
+
if (match) {
|
|
1027
|
+
cards.push({
|
|
1028
|
+
pergunta: match[1].trim(),
|
|
1029
|
+
resposta: match[2].trim(),
|
|
1030
|
+
deck
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return cards;
|
|
1035
|
+
}
|
|
1036
|
+
async function collectMarkdownFiles(dir) {
|
|
1037
|
+
if (!await fs9.pathExists(dir)) return [];
|
|
1038
|
+
const files = [];
|
|
1039
|
+
const stack = [dir];
|
|
1040
|
+
while (stack.length > 0) {
|
|
1041
|
+
const current = stack.pop();
|
|
1042
|
+
for (const entry of await fs9.readdir(current, { withFileTypes: true })) {
|
|
1043
|
+
const full = path12.join(current, entry.name);
|
|
1044
|
+
if (entry.isDirectory()) stack.push(full);
|
|
1045
|
+
else if (entry.name.endsWith(".md")) files.push(full);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return files;
|
|
1049
|
+
}
|
|
1050
|
+
async function collectAllFlashcards(vault) {
|
|
1051
|
+
const cards = [];
|
|
1052
|
+
const stack = [vault.root];
|
|
1053
|
+
while (stack.length > 0) {
|
|
1054
|
+
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;
|
|
1057
|
+
const full = path12.join(current, entry.name);
|
|
1058
|
+
if (!entry.isDirectory()) continue;
|
|
1059
|
+
if (entry.name.toLowerCase() === "flashcards") {
|
|
1060
|
+
const deck = path12.basename(path12.dirname(full));
|
|
1061
|
+
for (const file of await collectMarkdownFiles(full)) {
|
|
1062
|
+
cards.push(...parseFlashcards(await fs9.readFile(file, "utf8"), deck));
|
|
1063
|
+
}
|
|
1064
|
+
} else {
|
|
1065
|
+
stack.push(full);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return cards;
|
|
1070
|
+
}
|
|
1071
|
+
async function exportAnki(vault) {
|
|
1072
|
+
const cards = await collectAllFlashcards(vault);
|
|
1073
|
+
const lines = [
|
|
1074
|
+
"#separator:tab",
|
|
1075
|
+
"#html:false",
|
|
1076
|
+
"#deck column:3",
|
|
1077
|
+
...cards.map(
|
|
1078
|
+
(card) => `${card.pergunta.replace(/\t/g, " ")} ${card.resposta.replace(/\t/g, " ")} FATEC::${card.deck.replace(/\t/g, " ")}`
|
|
1079
|
+
)
|
|
1080
|
+
];
|
|
1081
|
+
const file = path12.join(
|
|
1082
|
+
vault.root,
|
|
1083
|
+
"flashcards",
|
|
1084
|
+
`anki-export-${todayISO()}.txt`
|
|
1085
|
+
);
|
|
1086
|
+
await fs9.outputFile(file, lines.join("\n"), "utf8");
|
|
1087
|
+
return { file, total: cards.length };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/commands/note-factory.ts
|
|
1091
|
+
import path14 from "path";
|
|
1092
|
+
import { Command as Command9 } from "commander";
|
|
1093
|
+
import fs11 from "fs-extra";
|
|
1094
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
1095
|
+
|
|
1096
|
+
// src/services/notes.ts
|
|
1097
|
+
import path13 from "path";
|
|
1098
|
+
import fs10 from "fs-extra";
|
|
1099
|
+
var NOTE_SPECS = {
|
|
1100
|
+
aula: { template: "aula", folder: "Aulas", prefix: "aula" },
|
|
1101
|
+
resumo: { template: "resumo", folder: "Resumos", prefix: "resumo" },
|
|
1102
|
+
prova: { template: "prova", folder: "Provas", prefix: "prova" },
|
|
1103
|
+
trabalho: { template: "trabalho", folder: "Trabalhos", prefix: "trabalho" },
|
|
1104
|
+
projeto: { template: "projeto", folder: "Projetos", prefix: "projeto" },
|
|
1105
|
+
flashcard: {
|
|
1106
|
+
template: "flashcard",
|
|
1107
|
+
folder: "Flashcards",
|
|
1108
|
+
prefix: "flashcards"
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
async function createNote(vault, input5) {
|
|
1112
|
+
const spec = NOTE_SPECS[input5.kind];
|
|
1113
|
+
const dir = path13.join(
|
|
1114
|
+
disciplinaDir(vault.root, input5.semestre, input5.disciplina),
|
|
1115
|
+
spec.folder
|
|
1116
|
+
);
|
|
1117
|
+
await fs10.ensureDir(dir);
|
|
1118
|
+
const filename = `${todayISO()}-${spec.prefix}-${slugify(input5.titulo)}.md`;
|
|
1119
|
+
const destination = path13.join(dir, filename);
|
|
1120
|
+
await renderToFile(
|
|
1121
|
+
spec.template,
|
|
1122
|
+
{
|
|
1123
|
+
titulo: input5.titulo,
|
|
1124
|
+
disciplina: input5.disciplina,
|
|
1125
|
+
semestre: input5.semestre,
|
|
1126
|
+
data: todayISO(),
|
|
1127
|
+
...input5.extra
|
|
1128
|
+
},
|
|
1129
|
+
destination,
|
|
1130
|
+
vault.root
|
|
1131
|
+
);
|
|
1132
|
+
return destination;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/services/ai.ts
|
|
1136
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1137
|
+
var AI_MODEL = "claude-opus-4-8";
|
|
1138
|
+
function anthropicProvider() {
|
|
1139
|
+
if (!process.env["ANTHROPIC_API_KEY"]) {
|
|
1140
|
+
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-..."'
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
const client = new Anthropic();
|
|
1145
|
+
return {
|
|
1146
|
+
async generate(prompt, material) {
|
|
1147
|
+
try {
|
|
1148
|
+
const stream = client.messages.stream({
|
|
1149
|
+
model: AI_MODEL,
|
|
1150
|
+
max_tokens: 64e3,
|
|
1151
|
+
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.",
|
|
1153
|
+
messages: [
|
|
1154
|
+
{
|
|
1155
|
+
role: "user",
|
|
1156
|
+
content: `${prompt}
|
|
1157
|
+
|
|
1158
|
+
<material>
|
|
1159
|
+
${material}
|
|
1160
|
+
</material>`
|
|
1161
|
+
}
|
|
1162
|
+
]
|
|
1163
|
+
});
|
|
1164
|
+
const message = await stream.finalMessage();
|
|
1165
|
+
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (error instanceof Anthropic.AuthenticationError) {
|
|
1168
|
+
throw new Error("ANTHROPIC_API_KEY inv\xE1lida. Verifique a chave.");
|
|
1169
|
+
}
|
|
1170
|
+
if (error instanceof Anthropic.RateLimitError) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
"Limite de requisi\xE7\xF5es da API atingido. Tente novamente em instantes."
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (error instanceof Anthropic.APIConnectionError) {
|
|
1176
|
+
throw new Error(
|
|
1177
|
+
"Sem conex\xE3o com a API da Anthropic. Verifique sua internet."
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function resumoPrompt(disciplina, tema) {
|
|
1186
|
+
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
|
+
}
|
|
1188
|
+
function flashcardsPrompt(disciplina, tema) {
|
|
1189
|
+
return `Gere de 10 a 20 flashcards sobre "${tema}" para a disciplina "${disciplina}", baseados no material fornecido. Formato obrigat\xF3rio: uma linha por card, "pergunta :: resposta" (compat\xEDvel com o plugin Spaced Repetition do Obsidian). Perguntas objetivas, respostas curtas. N\xE3o use cabe\xE7alhos nem numera\xE7\xE3o \u2014 apenas as linhas de cards.`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/utils/dates.ts
|
|
1193
|
+
function isValidISODate(value) {
|
|
1194
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
1195
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
1196
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
1197
|
+
}
|
|
1198
|
+
function addDaysISO(iso, days) {
|
|
1199
|
+
const date = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
1200
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
1201
|
+
return date.toISOString().slice(0, 10);
|
|
1202
|
+
}
|
|
1203
|
+
var DATE_HINT = "Use uma data v\xE1lida no formato YYYY-MM-DD.";
|
|
1204
|
+
|
|
1205
|
+
// src/commands/note-factory.ts
|
|
1206
|
+
async function readMaterial(dir) {
|
|
1207
|
+
if (!await fs11.pathExists(dir)) return "";
|
|
1208
|
+
const parts = [];
|
|
1209
|
+
for (const file of (await fs11.readdir(dir)).filter((f) => f.endsWith(".md"))) {
|
|
1210
|
+
parts.push(await fs11.readFile(path14.join(dir, file), "utf8"));
|
|
1211
|
+
}
|
|
1212
|
+
return parts.join("\n\n---\n\n");
|
|
1213
|
+
}
|
|
1214
|
+
function noteCommand(spec) {
|
|
1215
|
+
const command = new Command9(spec.name).description(spec.description).argument("[titulo]", "t\xEDtulo da nota").option("-d, --disciplina <nome>", "disciplina (evita o prompt)").option("-s, --semestre <n>", "semestre (padr\xE3o: semestre atual)").option("-c, --commit", "faz git commit no vault ap\xF3s criar");
|
|
1216
|
+
if (spec.extraField) {
|
|
1217
|
+
command.option(
|
|
1218
|
+
`--${spec.extraField.flag} <valor>`,
|
|
1219
|
+
spec.extraField.flagDescription
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (spec.ai) {
|
|
1223
|
+
command.option(
|
|
1224
|
+
"--ai",
|
|
1225
|
+
`gera o conte\xFAdo com IA a partir de ${spec.ai.sourceFolder}/ (requer ANTHROPIC_API_KEY)`
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
command.action(
|
|
1229
|
+
async (titulo, options) => {
|
|
1230
|
+
const vault = await requireVault();
|
|
1231
|
+
const matriz = await loadMatriz(vault.root);
|
|
1232
|
+
const semestre = options["semestre"] ? Number(options["semestre"]) : await pickSemestre(vault.config.semestreAtual);
|
|
1233
|
+
const disciplina = await pickDisciplina(
|
|
1234
|
+
matriz,
|
|
1235
|
+
semestre,
|
|
1236
|
+
options["disciplina"]
|
|
1237
|
+
);
|
|
1238
|
+
const tituloFinal = titulo ?? await askTitulo(spec.tituloPrompt);
|
|
1239
|
+
const extra = {};
|
|
1240
|
+
if (spec.extraField) {
|
|
1241
|
+
const field = spec.extraField;
|
|
1242
|
+
const camel = field.flag.replace(
|
|
1243
|
+
/-(\w)/g,
|
|
1244
|
+
(_, c) => c.toUpperCase()
|
|
1245
|
+
);
|
|
1246
|
+
let value = options[camel];
|
|
1247
|
+
if (value !== void 0 && field.isDate && !isValidISODate(value)) {
|
|
1248
|
+
throw new Error(`Data inv\xE1lida: "${value}". ${DATE_HINT}`);
|
|
1249
|
+
}
|
|
1250
|
+
value ??= await input2({
|
|
1251
|
+
message: field.prompt,
|
|
1252
|
+
default: todayISO(),
|
|
1253
|
+
validate: (v) => !field.isDate || isValidISODate(v) || DATE_HINT
|
|
1254
|
+
});
|
|
1255
|
+
extra[field.key] = value;
|
|
1256
|
+
}
|
|
1257
|
+
let conteudoIA;
|
|
1258
|
+
if (spec.ai && options["ai"]) {
|
|
1259
|
+
const provider = anthropicProvider();
|
|
1260
|
+
const sourceDir = path14.join(
|
|
1261
|
+
disciplinaDir(vault.root, semestre, disciplina),
|
|
1262
|
+
spec.ai.sourceFolder
|
|
1263
|
+
);
|
|
1264
|
+
const spinAi = spinner(
|
|
1265
|
+
`Gerando conte\xFAdo com IA (${spec.ai.sourceFolder}/)...`
|
|
1266
|
+
).start();
|
|
1267
|
+
try {
|
|
1268
|
+
const material = await readMaterial(sourceDir);
|
|
1269
|
+
conteudoIA = await provider.generate(
|
|
1270
|
+
spec.ai.buildPrompt(disciplina, tituloFinal),
|
|
1271
|
+
material || "(sem anota\xE7\xF5es \u2014 use seu conhecimento)"
|
|
1272
|
+
);
|
|
1273
|
+
spinAi.succeed("Conte\xFAdo gerado pela IA!");
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
spinAi.fail("Falha na gera\xE7\xE3o com IA.");
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const spin = spinner("Criando nota...").start();
|
|
1280
|
+
try {
|
|
1281
|
+
const file = await createNote(vault, {
|
|
1282
|
+
kind: spec.kind,
|
|
1283
|
+
semestre,
|
|
1284
|
+
disciplina,
|
|
1285
|
+
titulo: tituloFinal,
|
|
1286
|
+
extra
|
|
1287
|
+
});
|
|
1288
|
+
if (conteudoIA) {
|
|
1289
|
+
await fs11.appendFile(file, `
|
|
1290
|
+
## Gerado por IA
|
|
1291
|
+
|
|
1292
|
+
${conteudoIA}
|
|
1293
|
+
`);
|
|
1294
|
+
}
|
|
1295
|
+
spin.succeed(`Pronto! Nota criada em:`);
|
|
1296
|
+
logger.item(path14.relative(vault.root, file));
|
|
1297
|
+
if (options["commit"]) {
|
|
1298
|
+
const committed = await commitVault(
|
|
1299
|
+
vault.root,
|
|
1300
|
+
`minerva: ${spec.name} "${tituloFinal}" (${disciplina})`
|
|
1301
|
+
);
|
|
1302
|
+
if (committed) logger.success("Commit criado no vault.");
|
|
1303
|
+
else logger.info("Nada novo para commitar.");
|
|
1304
|
+
}
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
spin.fail("N\xE3o consegui criar a nota.");
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
);
|
|
1311
|
+
return command;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/commands/notes.ts
|
|
1315
|
+
function aulaCommand() {
|
|
1316
|
+
return noteCommand({
|
|
1317
|
+
kind: "aula",
|
|
1318
|
+
name: "aula",
|
|
1319
|
+
description: "Cria a anota\xE7\xE3o de uma aula",
|
|
1320
|
+
tituloPrompt: "T\xEDtulo da aula:"
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
function resumoCommand() {
|
|
1324
|
+
return noteCommand({
|
|
1325
|
+
kind: "resumo",
|
|
1326
|
+
name: "resumo",
|
|
1327
|
+
description: "Cria um resumo de estudo",
|
|
1328
|
+
tituloPrompt: "Tema do resumo:",
|
|
1329
|
+
ai: { sourceFolder: "Aulas", buildPrompt: resumoPrompt }
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function provaCommand() {
|
|
1333
|
+
return noteCommand({
|
|
1334
|
+
kind: "prova",
|
|
1335
|
+
name: "prova",
|
|
1336
|
+
description: "Agenda e planeja uma prova",
|
|
1337
|
+
tituloPrompt: "Nome da prova (ex.: P1):",
|
|
1338
|
+
extraField: {
|
|
1339
|
+
key: "dataProva",
|
|
1340
|
+
prompt: "Data da prova (YYYY-MM-DD):",
|
|
1341
|
+
flag: "data",
|
|
1342
|
+
flagDescription: "data da prova (YYYY-MM-DD)",
|
|
1343
|
+
isDate: true
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function trabalhoCommand() {
|
|
1348
|
+
return noteCommand({
|
|
1349
|
+
kind: "trabalho",
|
|
1350
|
+
name: "trabalho",
|
|
1351
|
+
description: "Cria um trabalho acad\xEAmico",
|
|
1352
|
+
tituloPrompt: "T\xEDtulo do trabalho:",
|
|
1353
|
+
extraField: {
|
|
1354
|
+
key: "entrega",
|
|
1355
|
+
prompt: "Data de entrega (YYYY-MM-DD):",
|
|
1356
|
+
flag: "entrega",
|
|
1357
|
+
flagDescription: "data de entrega (YYYY-MM-DD)",
|
|
1358
|
+
isDate: true
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function projetoCommand() {
|
|
1363
|
+
return noteCommand({
|
|
1364
|
+
kind: "projeto",
|
|
1365
|
+
name: "projeto",
|
|
1366
|
+
description: "Cria um projeto",
|
|
1367
|
+
tituloPrompt: "Nome do projeto:"
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
function flashcardsNewCommand() {
|
|
1371
|
+
return noteCommand({
|
|
1372
|
+
kind: "flashcard",
|
|
1373
|
+
name: "new",
|
|
1374
|
+
description: "Cria um deck de flashcards",
|
|
1375
|
+
tituloPrompt: "Tema dos flashcards:",
|
|
1376
|
+
ai: { sourceFolder: "Resumos", buildPrompt: flashcardsPrompt }
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/flashcards.ts
|
|
1381
|
+
function flashcardsCommand() {
|
|
1382
|
+
const command = new Command10("flashcards").description(
|
|
1383
|
+
"Cria decks de flashcards ou exporta para o Anki"
|
|
1384
|
+
);
|
|
1385
|
+
const create = flashcardsNewCommand();
|
|
1386
|
+
command.addCommand(create, { isDefault: true });
|
|
1387
|
+
command.command("export").description("Exporta todos os flashcards para um TSV import\xE1vel no Anki").action(async () => {
|
|
1388
|
+
const vault = await requireVault();
|
|
1389
|
+
const spin = spinner("Coletando flashcards do vault...").start();
|
|
1390
|
+
try {
|
|
1391
|
+
const { file, total } = await exportAnki(vault);
|
|
1392
|
+
spin.succeed(`${total} flashcard(s) exportado(s)!`);
|
|
1393
|
+
logger.item(path15.relative(vault.root, file));
|
|
1394
|
+
logger.info("No Anki: Arquivo \u2192 Importar e selecione esse arquivo.");
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
spin.fail("Falha na exporta\xE7\xE3o.");
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
return command;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/commands/init.ts
|
|
1404
|
+
import path16 from "path";
|
|
1405
|
+
import { execFile as execFile2 } from "child_process";
|
|
1406
|
+
import { promisify as promisify2 } from "util";
|
|
1407
|
+
import { Command as Command11 } from "commander";
|
|
1408
|
+
import fs12 from "fs-extra";
|
|
1409
|
+
import { input as input3, number as number2 } from "@inquirer/prompts";
|
|
1410
|
+
var exec2 = promisify2(execFile2);
|
|
1411
|
+
var GITIGNORE = `.obsidian/workspace*
|
|
1412
|
+
.obsidian/cache
|
|
1413
|
+
.trash/
|
|
1414
|
+
.DS_Store
|
|
1415
|
+
`;
|
|
1416
|
+
var ROADMAP = `# \u{1F5FA}\uFE0F Roadmap da Gradua\xE7\xE3o
|
|
1417
|
+
|
|
1418
|
+
- [ ] 1\xBA semestre conclu\xEDdo
|
|
1419
|
+
- [ ] 2\xBA semestre conclu\xEDdo
|
|
1420
|
+
- [ ] 3\xBA semestre conclu\xEDdo
|
|
1421
|
+
- [ ] Est\xE1gio conquistado
|
|
1422
|
+
- [ ] 4\xBA semestre conclu\xEDdo
|
|
1423
|
+
- [ ] 5\xBA semestre conclu\xEDdo
|
|
1424
|
+
- [ ] Trabalho de Gradua\xE7\xE3o entregue
|
|
1425
|
+
- [ ] 6\xBA semestre conclu\xEDdo
|
|
1426
|
+
- [ ] \u{1F393} Formado!
|
|
1427
|
+
`;
|
|
1428
|
+
async function initGit(root) {
|
|
1429
|
+
try {
|
|
1430
|
+
await exec2("git", ["init"], { cwd: root });
|
|
1431
|
+
return true;
|
|
1432
|
+
} catch {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async function writeIfMissing(file, content) {
|
|
1437
|
+
if (!await fs12.pathExists(file)) {
|
|
1438
|
+
await fs12.outputFile(file, content, "utf8");
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function initCommand() {
|
|
1442
|
+
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
|
+
async (pasta, options) => {
|
|
1444
|
+
logger.title("Minerva CLI \u2014 vamos montar sua gradua\xE7\xE3o!");
|
|
1445
|
+
const aluno = options.aluno ?? await input3({ message: "Seu nome:", default: "Estudante" });
|
|
1446
|
+
const semestreAtual = options.semestre !== void 0 ? Number(options.semestre) : await number2({
|
|
1447
|
+
message: "Em qual semestre voc\xEA est\xE1? (1-6)",
|
|
1448
|
+
default: 1,
|
|
1449
|
+
min: 1,
|
|
1450
|
+
max: 6
|
|
1451
|
+
}) ?? 1;
|
|
1452
|
+
const root = path16.resolve(pasta);
|
|
1453
|
+
const config = {
|
|
1454
|
+
versao: APP_VERSION,
|
|
1455
|
+
aluno,
|
|
1456
|
+
semestreAtual,
|
|
1457
|
+
criadoEm: todayISO(),
|
|
1458
|
+
horasEstudadas: 0
|
|
1459
|
+
};
|
|
1460
|
+
const spin = spinner("Criando estrutura de pastas...").start();
|
|
1461
|
+
try {
|
|
1462
|
+
await fs12.ensureDir(root);
|
|
1463
|
+
await scaffoldVaultFolders(root);
|
|
1464
|
+
await saveConfig(root, config);
|
|
1465
|
+
spin.text = "Salvando matriz curricular...";
|
|
1466
|
+
await saveMatriz(root, matrizOficial);
|
|
1467
|
+
spin.text = "Copiando templates...";
|
|
1468
|
+
await scaffoldTemplates(root);
|
|
1469
|
+
spin.text = "Criando semestres e disciplinas...";
|
|
1470
|
+
const totalDisciplinas = await scaffoldSemestres(root, matrizOficial);
|
|
1471
|
+
spin.text = "Preparando vault do Obsidian...";
|
|
1472
|
+
await scaffoldObsidian(root);
|
|
1473
|
+
spin.text = "Gerando README e dashboard...";
|
|
1474
|
+
await writeIfMissing(
|
|
1475
|
+
path16.join(root, "README.md"),
|
|
1476
|
+
await renderTemplate("readme", {
|
|
1477
|
+
...config,
|
|
1478
|
+
curso: matrizOficial.curso,
|
|
1479
|
+
instituicao: matrizOficial.instituicao,
|
|
1480
|
+
semestres: matrizOficial.semestres
|
|
1481
|
+
})
|
|
1482
|
+
);
|
|
1483
|
+
await writeIfMissing(path16.join(root, ".gitignore"), GITIGNORE);
|
|
1484
|
+
await writeIfMissing(
|
|
1485
|
+
path16.join(root, "roadmap", "Roadmap.md"),
|
|
1486
|
+
ROADMAP
|
|
1487
|
+
);
|
|
1488
|
+
await generateDashboard({ root, config }, matrizOficial);
|
|
1489
|
+
spin.text = "Inicializando Git...";
|
|
1490
|
+
const gitOk = await initGit(root);
|
|
1491
|
+
spin.succeed("Vault criado com sucesso!");
|
|
1492
|
+
logger.blank();
|
|
1493
|
+
logger.success(`\u{1F4C1} ${root}`);
|
|
1494
|
+
logger.item(`${totalDisciplinas} disciplinas em 6 semestres`);
|
|
1495
|
+
logger.item("Dashboard em dashboard/Dashboard.md");
|
|
1496
|
+
logger.item("Abra a pasta como vault no Obsidian \u{1F7E3}");
|
|
1497
|
+
if (!gitOk) {
|
|
1498
|
+
logger.warn("Git n\xE3o encontrado \u2014 reposit\xF3rio n\xE3o inicializado.");
|
|
1499
|
+
}
|
|
1500
|
+
logger.blank();
|
|
1501
|
+
logger.info(`Pr\xF3ximo passo: cd ${pasta} && minerva dashboard`);
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
spin.fail("Falha ao criar o vault.");
|
|
1504
|
+
throw error;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/commands/nota.ts
|
|
1511
|
+
import chalk2 from "chalk";
|
|
1512
|
+
import { Command as Command12 } from "commander";
|
|
1513
|
+
import { input as input4 } from "@inquirer/prompts";
|
|
1514
|
+
|
|
1515
|
+
// src/services/notas.ts
|
|
1516
|
+
import path17 from "path";
|
|
1517
|
+
import fs13 from "fs-extra";
|
|
1518
|
+
import { z as z3 } from "zod";
|
|
1519
|
+
var NOTAS_FILE = path17.join("curriculo", "notas.json");
|
|
1520
|
+
var MEDIA_APROVACAO = 6;
|
|
1521
|
+
var notaSchema = z3.object({
|
|
1522
|
+
disciplina: z3.string(),
|
|
1523
|
+
avaliacao: z3.string(),
|
|
1524
|
+
valor: z3.number().min(0).max(10),
|
|
1525
|
+
peso: z3.number().positive().default(1)
|
|
1526
|
+
});
|
|
1527
|
+
var notasSchema = z3.array(notaSchema);
|
|
1528
|
+
async function loadNotas(root) {
|
|
1529
|
+
const file = path17.join(root, NOTAS_FILE);
|
|
1530
|
+
if (!await fs13.pathExists(file)) return [];
|
|
1531
|
+
const raw = await fs13.readJson(file);
|
|
1532
|
+
return notasSchema.parse(raw);
|
|
1533
|
+
}
|
|
1534
|
+
async function registrarNota(root, nota) {
|
|
1535
|
+
const notas = await loadNotas(root);
|
|
1536
|
+
const existing = notas.findIndex(
|
|
1537
|
+
(n) => n.disciplina === nota.disciplina && n.avaliacao.toLowerCase() === nota.avaliacao.toLowerCase()
|
|
1538
|
+
);
|
|
1539
|
+
if (existing >= 0) notas[existing] = nota;
|
|
1540
|
+
else notas.push(nota);
|
|
1541
|
+
await fs13.outputJson(path17.join(root, NOTAS_FILE), notas, { spaces: 4 });
|
|
1542
|
+
}
|
|
1543
|
+
function media(notas) {
|
|
1544
|
+
const pesoTotal = notas.reduce((sum, n) => sum + n.peso, 0);
|
|
1545
|
+
if (pesoTotal === 0) return 0;
|
|
1546
|
+
const soma = notas.reduce((sum, n) => sum + n.valor * n.peso, 0);
|
|
1547
|
+
return Math.round(soma / pesoTotal * 100) / 100;
|
|
1548
|
+
}
|
|
1549
|
+
function boletim(notas) {
|
|
1550
|
+
const porDisciplina = /* @__PURE__ */ new Map();
|
|
1551
|
+
for (const nota of notas) {
|
|
1552
|
+
const list = porDisciplina.get(nota.disciplina) ?? [];
|
|
1553
|
+
list.push(nota);
|
|
1554
|
+
porDisciplina.set(nota.disciplina, list);
|
|
1555
|
+
}
|
|
1556
|
+
return [...porDisciplina.entries()].map(([disciplina, list]) => {
|
|
1557
|
+
const m = media(list);
|
|
1558
|
+
return {
|
|
1559
|
+
disciplina,
|
|
1560
|
+
notas: list,
|
|
1561
|
+
media: m,
|
|
1562
|
+
situacao: m >= MEDIA_APROVACAO ? "aprovado" : m >= MEDIA_APROVACAO - 2 ? "em risco" : "reprovado"
|
|
1563
|
+
};
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/commands/nota.ts
|
|
1568
|
+
var SITUACAO_COLOR = {
|
|
1569
|
+
aprovado: chalk2.green,
|
|
1570
|
+
"em risco": chalk2.yellow,
|
|
1571
|
+
reprovado: chalk2.red
|
|
1572
|
+
};
|
|
1573
|
+
function notaCommand() {
|
|
1574
|
+
const command = new Command12("nota").description(
|
|
1575
|
+
"Registra notas e mostra o boletim com m\xE9dias"
|
|
1576
|
+
);
|
|
1577
|
+
command.command("boletim", { isDefault: true }).description("Mostra m\xE9dias e situa\xE7\xE3o por disciplina").option("--json", "sa\xEDda em JSON").action(async (options) => {
|
|
1578
|
+
const vault = await requireVault();
|
|
1579
|
+
const notas = await loadNotas(vault.root);
|
|
1580
|
+
const linhas = boletim(notas);
|
|
1581
|
+
if (options.json) {
|
|
1582
|
+
console.log(JSON.stringify(linhas, null, 2));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (linhas.length === 0) {
|
|
1586
|
+
logger.info("Nenhuma nota registrada. Use: minerva nota add");
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
logger.title(`Boletim (m\xE9dia m\xEDnima: ${MEDIA_APROVACAO})`);
|
|
1590
|
+
for (const linha of linhas) {
|
|
1591
|
+
const color = SITUACAO_COLOR[linha.situacao];
|
|
1592
|
+
logger.info(
|
|
1593
|
+
`${linha.disciplina}: m\xE9dia ${chalk2.bold(linha.media)} \u2014 ${color(linha.situacao)}`
|
|
1594
|
+
);
|
|
1595
|
+
for (const nota of linha.notas) {
|
|
1596
|
+
logger.item(`${nota.avaliacao}: ${nota.valor} (peso ${nota.peso})`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
command.command("add").description("Registra a nota de uma avalia\xE7\xE3o").argument("[valor]", "nota de 0 a 10").option("-d, --disciplina <nome>", "disciplina").option("-a, --avaliacao <nome>", "nome da avalia\xE7\xE3o (ex.: P1)").option("-p, --peso <n>", "peso da avalia\xE7\xE3o", "1").option("-s, --semestre <n>", "semestre da disciplina").action(
|
|
1601
|
+
async (valor, options) => {
|
|
1602
|
+
const vault = await requireVault();
|
|
1603
|
+
const matriz = await loadMatriz(vault.root);
|
|
1604
|
+
const semestre = options.semestre ? Number(options.semestre) : vault.config.semestreAtual;
|
|
1605
|
+
const disciplina = await pickDisciplina(
|
|
1606
|
+
matriz,
|
|
1607
|
+
semestre,
|
|
1608
|
+
options.disciplina
|
|
1609
|
+
);
|
|
1610
|
+
const avaliacao = options.avaliacao ?? await input4({
|
|
1611
|
+
message: "Avalia\xE7\xE3o (ex.: P1, Trabalho 1):",
|
|
1612
|
+
validate: (v) => v.trim().length > 0 || "Informe um nome."
|
|
1613
|
+
});
|
|
1614
|
+
const valorFinal = Number(
|
|
1615
|
+
(valor ?? await input4({
|
|
1616
|
+
message: "Nota (0-10):",
|
|
1617
|
+
validate: (v) => {
|
|
1618
|
+
const n = Number(v.replace(",", "."));
|
|
1619
|
+
return Number.isFinite(n) && n >= 0 && n <= 10 || "Informe uma nota entre 0 e 10.";
|
|
1620
|
+
}
|
|
1621
|
+
})).replace(",", ".")
|
|
1622
|
+
);
|
|
1623
|
+
await registrarNota(vault.root, {
|
|
1624
|
+
disciplina,
|
|
1625
|
+
avaliacao,
|
|
1626
|
+
valor: valorFinal,
|
|
1627
|
+
peso: Number(options.peso)
|
|
1628
|
+
});
|
|
1629
|
+
const notas = await loadNotas(vault.root);
|
|
1630
|
+
const linha = boletim(notas).find((b) => b.disciplina === disciplina);
|
|
1631
|
+
logger.success(
|
|
1632
|
+
`${avaliacao} = ${valorFinal} registrada em ${disciplina}.`
|
|
1633
|
+
);
|
|
1634
|
+
logger.info(
|
|
1635
|
+
`M\xE9dia atual: ${linha.media} (${SITUACAO_COLOR[linha.situacao](linha.situacao)})`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
);
|
|
1639
|
+
return command;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/commands/open.ts
|
|
1643
|
+
import path18 from "path";
|
|
1644
|
+
import { execFile as execFile3 } from "child_process";
|
|
1645
|
+
import { promisify as promisify3 } from "util";
|
|
1646
|
+
import { Command as Command13 } from "commander";
|
|
1647
|
+
var exec3 = promisify3(execFile3);
|
|
1648
|
+
async function openUrl(url) {
|
|
1649
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1650
|
+
await exec3(opener, [url], { shell: process.platform === "win32" });
|
|
1651
|
+
}
|
|
1652
|
+
function openCommand() {
|
|
1653
|
+
return new Command13("open").description("Abre o dashboard (ou um arquivo) no Obsidian").argument("[arquivo]", "caminho relativo dentro do vault").action(async (arquivo) => {
|
|
1654
|
+
const vault = await requireVault();
|
|
1655
|
+
const target = path18.join(
|
|
1656
|
+
vault.root,
|
|
1657
|
+
arquivo ?? path18.join("dashboard", "Dashboard.md")
|
|
1658
|
+
);
|
|
1659
|
+
const url = `obsidian://open?path=${encodeURIComponent(target)}`;
|
|
1660
|
+
try {
|
|
1661
|
+
await openUrl(url);
|
|
1662
|
+
logger.success("Abrindo no Obsidian...");
|
|
1663
|
+
} catch {
|
|
1664
|
+
logger.error(
|
|
1665
|
+
"N\xE3o consegui abrir o Obsidian. Ele est\xE1 instalado? O vault j\xE1 foi aberto nele ao menos uma vez?"
|
|
1666
|
+
);
|
|
1667
|
+
logger.info(`Arquivo: ${target}`);
|
|
1668
|
+
process.exitCode = 1;
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/commands/pendencias.ts
|
|
1674
|
+
import chalk3 from "chalk";
|
|
1675
|
+
import { Command as Command14 } from "commander";
|
|
1676
|
+
|
|
1677
|
+
// src/services/pendencias.ts
|
|
1678
|
+
import path19 from "path";
|
|
1679
|
+
import fs14 from "fs-extra";
|
|
1680
|
+
async function scanFile(file, root, disciplina) {
|
|
1681
|
+
const content = await fs14.readFile(file, "utf8");
|
|
1682
|
+
const items = [];
|
|
1683
|
+
content.split("\n").forEach((line, index) => {
|
|
1684
|
+
const match = /^\s*[-*] \[ \]\s*(.*)$/.exec(line);
|
|
1685
|
+
if (match) {
|
|
1686
|
+
items.push({
|
|
1687
|
+
disciplina,
|
|
1688
|
+
arquivo: path19.relative(root, file),
|
|
1689
|
+
linha: index + 1,
|
|
1690
|
+
texto: match[1]?.trim() ?? ""
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
return items;
|
|
1695
|
+
}
|
|
1696
|
+
async function listPendencias(vault, matriz) {
|
|
1697
|
+
const semestre = getSemestre(matriz, vault.config.semestreAtual);
|
|
1698
|
+
const pendencias = [];
|
|
1699
|
+
for (const disciplina of semestre.disciplinas) {
|
|
1700
|
+
const dir = disciplinaDir(vault.root, semestre.numero, disciplina.nome);
|
|
1701
|
+
if (!await fs14.pathExists(dir)) continue;
|
|
1702
|
+
const stack = [dir];
|
|
1703
|
+
while (stack.length > 0) {
|
|
1704
|
+
const current = stack.pop();
|
|
1705
|
+
for (const entry of await fs14.readdir(current, { withFileTypes: true })) {
|
|
1706
|
+
const full = path19.join(current, entry.name);
|
|
1707
|
+
if (entry.isDirectory()) stack.push(full);
|
|
1708
|
+
else if (entry.name.endsWith(".md")) {
|
|
1709
|
+
pendencias.push(
|
|
1710
|
+
...await scanFile(full, vault.root, disciplina.nome)
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return pendencias;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/commands/pendencias.ts
|
|
1720
|
+
function pendenciasCommand() {
|
|
1721
|
+
return new Command14("pendencias").description("Lista as tarefas pendentes (- [ ]) do semestre atual").option("--json", "sa\xEDda em JSON").action(async (options) => {
|
|
1722
|
+
const vault = await requireVault();
|
|
1723
|
+
const matriz = await loadMatriz(vault.root);
|
|
1724
|
+
const pendencias = await listPendencias(vault, matriz);
|
|
1725
|
+
if (options.json) {
|
|
1726
|
+
console.log(JSON.stringify(pendencias, null, 2));
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (pendencias.length === 0) {
|
|
1730
|
+
logger.success("Nenhuma pend\xEAncia aberta. \u{1F389}");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
logger.title(`${pendencias.length} pend\xEAncia(s) aberta(s)`);
|
|
1734
|
+
let atual = "";
|
|
1735
|
+
for (const p of pendencias) {
|
|
1736
|
+
if (p.disciplina !== atual) {
|
|
1737
|
+
atual = p.disciplina;
|
|
1738
|
+
logger.info(chalk3.bold(atual));
|
|
1739
|
+
}
|
|
1740
|
+
logger.item(
|
|
1741
|
+
`${p.texto || "(sem descri\xE7\xE3o)"} ${chalk3.dim(`\u2014 ${p.arquivo}:${p.linha}`)}`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/commands/portfolio.ts
|
|
1748
|
+
import path20 from "path";
|
|
1749
|
+
import { Command as Command15 } from "commander";
|
|
1750
|
+
import fs15 from "fs-extra";
|
|
1751
|
+
async function collectProjects(root) {
|
|
1752
|
+
const items = [];
|
|
1753
|
+
const pessoais = path20.join(root, "projetos-pessoais");
|
|
1754
|
+
if (await fs15.pathExists(pessoais)) {
|
|
1755
|
+
for (const entry of await fs15.readdir(pessoais, {
|
|
1756
|
+
withFileTypes: true
|
|
1757
|
+
})) {
|
|
1758
|
+
if (entry.isDirectory() || entry.name.endsWith(".md")) {
|
|
1759
|
+
items.push({
|
|
1760
|
+
titulo: entry.name.replace(/\.md$/, ""),
|
|
1761
|
+
origem: "Projeto pessoal",
|
|
1762
|
+
caminho: path20.join("projetos-pessoais", entry.name)
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const matriz = await loadMatriz(root);
|
|
1768
|
+
for (const semestre of matriz.semestres) {
|
|
1769
|
+
for (const disciplina of semestre.disciplinas) {
|
|
1770
|
+
const dir = path20.join(
|
|
1771
|
+
disciplinaDir(root, semestre.numero, disciplina.nome),
|
|
1772
|
+
"Projetos"
|
|
1773
|
+
);
|
|
1774
|
+
if (!await fs15.pathExists(dir)) continue;
|
|
1775
|
+
for (const file of await fs15.readdir(dir)) {
|
|
1776
|
+
if (!file.endsWith(".md")) continue;
|
|
1777
|
+
items.push({
|
|
1778
|
+
titulo: file.replace(/\.md$/, ""),
|
|
1779
|
+
origem: `${disciplina.nome} (${semestre.numero}\xBA sem.)`,
|
|
1780
|
+
caminho: path20.relative(root, path20.join(dir, file))
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return items;
|
|
1786
|
+
}
|
|
1787
|
+
function portfolioCommand() {
|
|
1788
|
+
return new Command15("portfolio").description("Gera o \xEDndice do portf\xF3lio com todos os seus projetos").action(async () => {
|
|
1789
|
+
const vault = await requireVault();
|
|
1790
|
+
const spin = spinner("Reunindo seus projetos...").start();
|
|
1791
|
+
try {
|
|
1792
|
+
const items = await collectProjects(vault.root);
|
|
1793
|
+
const lines = [
|
|
1794
|
+
"# \u{1F4BC} Portf\xF3lio",
|
|
1795
|
+
"",
|
|
1796
|
+
`> Gerado por \`minerva portfolio\` em ${todayISO()}.`,
|
|
1797
|
+
"",
|
|
1798
|
+
"| Projeto | Origem | Nota |",
|
|
1799
|
+
"| ------- | ------ | ---- |",
|
|
1800
|
+
...items.map(
|
|
1801
|
+
(item) => `| ${item.titulo} | ${item.origem} | [abrir](../${item.caminho.split(path20.sep).join("/")}) |`
|
|
1802
|
+
),
|
|
1803
|
+
""
|
|
1804
|
+
];
|
|
1805
|
+
const destination = path20.join(vault.root, "portfolio", "Portfolio.md");
|
|
1806
|
+
await fs15.outputFile(destination, lines.join("\n"), "utf8");
|
|
1807
|
+
spin.succeed(`Portf\xF3lio atualizado com ${items.length} projeto(s)!`);
|
|
1808
|
+
logger.item(path20.relative(vault.root, destination));
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
spin.fail("Falha ao gerar o portf\xF3lio.");
|
|
1811
|
+
throw error;
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/commands/semana.ts
|
|
1817
|
+
import { Command as Command16 } from "commander";
|
|
1818
|
+
|
|
1819
|
+
// src/services/agenda.ts
|
|
1820
|
+
import path21 from "path";
|
|
1821
|
+
import fs16 from "fs-extra";
|
|
1822
|
+
var FRONTMATTER_DATES = [
|
|
1823
|
+
{ folder: "Provas", field: "data-prova", tipo: "prova" },
|
|
1824
|
+
{ folder: "Trabalhos", field: "entrega", tipo: "entrega" }
|
|
1825
|
+
];
|
|
1826
|
+
function extractDate(content, field) {
|
|
1827
|
+
const match = new RegExp(`^${field}:\\s*(\\d{4}-\\d{2}-\\d{2})`, "m").exec(
|
|
1828
|
+
content
|
|
1829
|
+
);
|
|
1830
|
+
return match ? match[1] : null;
|
|
1831
|
+
}
|
|
1832
|
+
function extractTitle(content, fallback) {
|
|
1833
|
+
const match = /^# .*?[—-]\s*(.+)$/m.exec(content);
|
|
1834
|
+
return match ? match[1].trim() : fallback;
|
|
1835
|
+
}
|
|
1836
|
+
async function proximosCompromissos(vault, matriz, dias = 7) {
|
|
1837
|
+
const semestre = getSemestre(matriz, vault.config.semestreAtual);
|
|
1838
|
+
const inicio = todayISO();
|
|
1839
|
+
const fim = addDaysISO(inicio, dias);
|
|
1840
|
+
const compromissos = [];
|
|
1841
|
+
for (const disciplina of semestre.disciplinas) {
|
|
1842
|
+
for (const spec of FRONTMATTER_DATES) {
|
|
1843
|
+
const dir = path21.join(
|
|
1844
|
+
disciplinaDir(vault.root, semestre.numero, disciplina.nome),
|
|
1845
|
+
spec.folder
|
|
1846
|
+
);
|
|
1847
|
+
if (!await fs16.pathExists(dir)) continue;
|
|
1848
|
+
for (const file of await fs16.readdir(dir)) {
|
|
1849
|
+
if (!file.endsWith(".md")) continue;
|
|
1850
|
+
const full = path21.join(dir, file);
|
|
1851
|
+
const content = await fs16.readFile(full, "utf8");
|
|
1852
|
+
const data = extractDate(content, spec.field);
|
|
1853
|
+
if (data && data >= inicio && data <= fim) {
|
|
1854
|
+
compromissos.push({
|
|
1855
|
+
tipo: spec.tipo,
|
|
1856
|
+
disciplina: disciplina.nome,
|
|
1857
|
+
titulo: extractTitle(content, file.replace(/\.md$/, "")),
|
|
1858
|
+
data,
|
|
1859
|
+
arquivo: path21.relative(vault.root, full)
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return compromissos.sort((a, b) => a.data.localeCompare(b.data));
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/commands/semana.ts
|
|
1869
|
+
function semanaCommand() {
|
|
1870
|
+
return new Command16("semana").description("Mostra provas, entregas e pend\xEAncias dos pr\xF3ximos dias").option("--dias <n>", "janela em dias", "7").option("--json", "sa\xEDda em JSON").action(async (options) => {
|
|
1871
|
+
const vault = await requireVault();
|
|
1872
|
+
const matriz = await loadMatriz(vault.root);
|
|
1873
|
+
const dias = Number(options.dias);
|
|
1874
|
+
const [compromissos, pendencias] = await Promise.all([
|
|
1875
|
+
proximosCompromissos(vault, matriz, dias),
|
|
1876
|
+
listPendencias(vault, matriz)
|
|
1877
|
+
]);
|
|
1878
|
+
if (options.json) {
|
|
1879
|
+
console.log(
|
|
1880
|
+
JSON.stringify(
|
|
1881
|
+
{ compromissos, totalPendencias: pendencias.length },
|
|
1882
|
+
null,
|
|
1883
|
+
2
|
|
1884
|
+
)
|
|
1885
|
+
);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
logger.title(`Sua semana (pr\xF3ximos ${dias} dias)`);
|
|
1889
|
+
if (compromissos.length === 0) {
|
|
1890
|
+
logger.success("Nenhuma prova ou entrega no per\xEDodo. \u{1F389}");
|
|
1891
|
+
}
|
|
1892
|
+
for (const c of compromissos) {
|
|
1893
|
+
const icon = c.tipo === "prova" ? "\u{1F3AF}" : "\u{1F4E6}";
|
|
1894
|
+
logger.item(`${c.data} ${icon} ${c.disciplina} \u2014 ${c.titulo}`);
|
|
1895
|
+
}
|
|
1896
|
+
logger.blank();
|
|
1897
|
+
if (pendencias.length > 0) {
|
|
1898
|
+
logger.warn(
|
|
1899
|
+
`${pendencias.length} pend\xEAncia(s) aberta(s) \u2014 veja "minerva pendencias".`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/commands/semestre.ts
|
|
1906
|
+
import { Command as Command17 } from "commander";
|
|
1907
|
+
function semestreCommand() {
|
|
1908
|
+
return new Command17("semestre").description("Mostra ou define o semestre atual").argument("[numero]", "novo semestre (1-6)").action(async (numero) => {
|
|
1909
|
+
const vault = await requireVault();
|
|
1910
|
+
const matriz = await loadMatriz(vault.root);
|
|
1911
|
+
if (numero === void 0) {
|
|
1912
|
+
const atual = getSemestre(matriz, vault.config.semestreAtual);
|
|
1913
|
+
logger.title(`${atual.numero}\xBA semestre`);
|
|
1914
|
+
logger.info(`Progresso do curso: ${progressBar(atual.numero - 1, 6)}`);
|
|
1915
|
+
for (const d of atual.disciplinas) {
|
|
1916
|
+
logger.item(`${d.nome} (${d.aulas} aulas)`);
|
|
1917
|
+
}
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const novo = getSemestre(matriz, Number(numero));
|
|
1921
|
+
vault.config.semestreAtual = novo.numero;
|
|
1922
|
+
await saveConfig(vault.root, vault.config);
|
|
1923
|
+
await generateDashboard(vault, matriz);
|
|
1924
|
+
logger.success(
|
|
1925
|
+
`Semestre atual definido para ${novo.numero}\xBA. Dashboard atualizado!`
|
|
1926
|
+
);
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/commands/stats.ts
|
|
1931
|
+
import chalk4 from "chalk";
|
|
1932
|
+
import { Command as Command18 } from "commander";
|
|
1933
|
+
|
|
1934
|
+
// src/services/stats.ts
|
|
1935
|
+
import path22 from "path";
|
|
1936
|
+
import fs17 from "fs-extra";
|
|
1937
|
+
var NOTE_FILENAME = /^(\d{4}-\d{2}-\d{2})-(aula|resumo|prova|trabalho|projeto|flashcards)-/;
|
|
1938
|
+
async function collectDates(root) {
|
|
1939
|
+
const dates = [];
|
|
1940
|
+
const stack = [root];
|
|
1941
|
+
while (stack.length > 0) {
|
|
1942
|
+
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;
|
|
1945
|
+
const full = path22.join(current, entry.name);
|
|
1946
|
+
if (entry.isDirectory()) stack.push(full);
|
|
1947
|
+
else {
|
|
1948
|
+
const match = NOTE_FILENAME.exec(entry.name);
|
|
1949
|
+
if (match) dates.push([match[1], match[2]]);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return dates;
|
|
1954
|
+
}
|
|
1955
|
+
async function collectStats(vault, dias = 30) {
|
|
1956
|
+
const entries = await collectDates(vault.root);
|
|
1957
|
+
const estudoLog = await loadEstudoLog(vault.root);
|
|
1958
|
+
const hoje = todayISO();
|
|
1959
|
+
const inicio = addDaysISO(hoje, -dias);
|
|
1960
|
+
const notasPorDia = {};
|
|
1961
|
+
const notasPorTipo = {};
|
|
1962
|
+
const diasAtivos = new Set(estudoLog.map((s) => s.data));
|
|
1963
|
+
for (const [data, tipo] of entries) {
|
|
1964
|
+
notasPorTipo[tipo] = (notasPorTipo[tipo] ?? 0) + 1;
|
|
1965
|
+
diasAtivos.add(data);
|
|
1966
|
+
if (data >= inicio && data <= hoje) {
|
|
1967
|
+
notasPorDia[data] = (notasPorDia[data] ?? 0) + 1;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
let streak = 0;
|
|
1971
|
+
for (let dia = hoje; diasAtivos.has(dia); dia = addDaysISO(dia, -1)) {
|
|
1972
|
+
streak += 1;
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
notasPorDia,
|
|
1976
|
+
notasPorTipo,
|
|
1977
|
+
streak,
|
|
1978
|
+
horasEstudadas: vault.config.horasEstudadas,
|
|
1979
|
+
totalNotas: entries.length
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/commands/stats.ts
|
|
1984
|
+
function cell(count) {
|
|
1985
|
+
if (count === 0) return chalk4.dim("\xB7");
|
|
1986
|
+
if (count === 1) return chalk4.green("\u25AA");
|
|
1987
|
+
if (count <= 3) return chalk4.green("\u25AE");
|
|
1988
|
+
return chalk4.bold.green("\u2588");
|
|
1989
|
+
}
|
|
1990
|
+
function statsCommand() {
|
|
1991
|
+
return new Command18("stats").description("Mostra estat\xEDsticas de const\xE2ncia (notas, streak, horas)").option("--dias <n>", "janela do gr\xE1fico em dias", "30").option("--json", "sa\xEDda em JSON").action(async (options) => {
|
|
1992
|
+
const vault = await requireVault();
|
|
1993
|
+
const dias = Number(options.dias);
|
|
1994
|
+
const stats = await collectStats(vault, dias);
|
|
1995
|
+
if (options.json) {
|
|
1996
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
logger.title("Suas estat\xEDsticas de estudo");
|
|
2000
|
+
logger.info(`\u{1F525} Streak: ${chalk4.bold(stats.streak)} dia(s) seguidos`);
|
|
2001
|
+
logger.info(`\u23F1\uFE0F Horas estudadas: ${stats.horasEstudadas}h`);
|
|
2002
|
+
logger.info(`\u{1F4DD} Notas criadas: ${stats.totalNotas}`);
|
|
2003
|
+
logger.blank();
|
|
2004
|
+
const hoje = todayISO();
|
|
2005
|
+
let graph = "";
|
|
2006
|
+
for (let i = dias - 1; i >= 0; i -= 1) {
|
|
2007
|
+
graph += cell(stats.notasPorDia[addDaysISO(hoje, -i)] ?? 0);
|
|
2008
|
+
}
|
|
2009
|
+
logger.info(`\xDAltimos ${dias} dias: ${graph}`);
|
|
2010
|
+
logger.blank();
|
|
2011
|
+
for (const [tipo, total] of Object.entries(stats.notasPorTipo).sort(
|
|
2012
|
+
(a, b) => b[1] - a[1]
|
|
2013
|
+
)) {
|
|
2014
|
+
logger.item(`${tipo}: ${total}`);
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/commands/update-matriz.ts
|
|
2020
|
+
import { Command as Command19 } from "commander";
|
|
2021
|
+
function updateMatrizCommand() {
|
|
2022
|
+
return new Command19("update-matriz").description("Atualiza a matriz curricular a partir do site da FATEC").option("--url <url>", "URL alternativa da matriz", MATRIZ_URL).action(async (options) => {
|
|
2023
|
+
const vault = await requireVault();
|
|
2024
|
+
const spin = spinner("Baixando matriz curricular...").start();
|
|
2025
|
+
try {
|
|
2026
|
+
const matriz = await fetchMatriz(options.url);
|
|
2027
|
+
await saveMatriz(vault.root, matriz);
|
|
2028
|
+
spin.succeed("Matriz curricular atualizada!");
|
|
2029
|
+
const total = matriz.semestres.reduce(
|
|
2030
|
+
(sum, s) => sum + s.disciplinas.length,
|
|
2031
|
+
0
|
|
2032
|
+
);
|
|
2033
|
+
logger.item(`${total} disciplinas em 6 semestres`);
|
|
2034
|
+
logger.item(`Fonte: ${matriz.fonte}`);
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
spin.fail("N\xE3o foi poss\xEDvel atualizar a matriz.");
|
|
2037
|
+
logger.warn(
|
|
2038
|
+
"A matriz embutida na CLI continua funcionando normalmente."
|
|
2039
|
+
);
|
|
2040
|
+
throw error;
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/program.ts
|
|
2046
|
+
function buildProgram() {
|
|
2047
|
+
const program = new Command20(APP_NAME).description(APP_DESCRIPTION).version(APP_VERSION, "-v, --version", "mostra a vers\xE3o").helpCommand("help [comando]", "mostra ajuda de um comando").showSuggestionAfterError();
|
|
2048
|
+
program.addCommand(initCommand());
|
|
2049
|
+
program.addCommand(dashboardCommand());
|
|
2050
|
+
program.addCommand(aulaCommand());
|
|
2051
|
+
program.addCommand(resumoCommand());
|
|
2052
|
+
program.addCommand(provaCommand());
|
|
2053
|
+
program.addCommand(trabalhoCommand());
|
|
2054
|
+
program.addCommand(projetoCommand());
|
|
2055
|
+
program.addCommand(portfolioCommand());
|
|
2056
|
+
program.addCommand(cheatsheetCommand());
|
|
2057
|
+
program.addCommand(flashcardsCommand());
|
|
2058
|
+
program.addCommand(semestreCommand());
|
|
2059
|
+
program.addCommand(disciplinaCommand());
|
|
2060
|
+
program.addCommand(estudoCommand());
|
|
2061
|
+
program.addCommand(pendenciasCommand());
|
|
2062
|
+
program.addCommand(semanaCommand());
|
|
2063
|
+
program.addCommand(notaCommand());
|
|
2064
|
+
program.addCommand(statsCommand());
|
|
2065
|
+
program.addCommand(openCommand());
|
|
2066
|
+
program.addCommand(commitCommand());
|
|
2067
|
+
program.addCommand(configCommand());
|
|
2068
|
+
program.addCommand(completionCommand());
|
|
2069
|
+
program.addCommand(updateMatrizCommand());
|
|
2070
|
+
program.addCommand(doctorCommand());
|
|
2071
|
+
program.command("version").description("mostra a vers\xE3o da Minerva CLI").action(() => {
|
|
2072
|
+
console.log(APP_VERSION);
|
|
2073
|
+
});
|
|
2074
|
+
return program;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/index.ts
|
|
2078
|
+
async function main() {
|
|
2079
|
+
try {
|
|
2080
|
+
await buildProgram().parseAsync(process.argv);
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
2083
|
+
logger.info("At\xE9 a pr\xF3xima! \u{1F44B}");
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2087
|
+
if (process.env["DEBUG"] && error instanceof Error) {
|
|
2088
|
+
console.error(chalk5.dim(error.stack ?? ""));
|
|
2089
|
+
}
|
|
2090
|
+
process.exitCode = 1;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
void main();
|
|
2094
|
+
export {
|
|
2095
|
+
buildProgram
|
|
2096
|
+
};
|
|
2097
|
+
//# sourceMappingURL=index.js.map
|