@soltaoverbo/cli 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/.turbo/turbo-build.log +13 -0
- package/dist/bin.js +345 -0
- package/package.json +22 -0
- package/src/bin.ts +75 -0
- package/src/commands/install.ts +51 -0
- package/src/commands/list.ts +59 -0
- package/src/commands/reset.ts +23 -0
- package/src/commands/start.ts +133 -0
- package/src/commands/stats.ts +28 -0
- package/src/ui.ts +14 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
> @soltaoverbo/cli@0.2.0 build C:\Users\aresn\OneDrive\Documentos\GitHub\verbo\packages\cli
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/bin.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: C:\Users\aresn\OneDrive\Documentos\GitHub\verbo\packages\cli\tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: node20
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mESM[39m [1mdist\bin.js [22m[32m11.08 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 26ms
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
// src/commands/install.ts
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core";
|
|
11
|
+
|
|
12
|
+
// src/ui.ts
|
|
13
|
+
var R = "\x1B[0m";
|
|
14
|
+
var bold = (s) => `\x1B[1m${s}${R}`;
|
|
15
|
+
var dim = (s) => `\x1B[2m${s}${R}`;
|
|
16
|
+
var green = (s) => `\x1B[32m${s}${R}`;
|
|
17
|
+
var yellow = (s) => `\x1B[33m${s}${R}`;
|
|
18
|
+
var cyan = (s) => `\x1B[36m${s}${R}`;
|
|
19
|
+
function progressBar(pct, width) {
|
|
20
|
+
const filled = Math.round(pct / 100 * width);
|
|
21
|
+
const empty = width - filled;
|
|
22
|
+
return `\x1B[32m${"\u2588".repeat(filled)}\x1B[2m${"\u2591".repeat(empty)}${R}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/commands/install.ts
|
|
26
|
+
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
27
|
+
var VERBO_PREFIX = "verbo \xB7 ";
|
|
28
|
+
function runInstall() {
|
|
29
|
+
if (!existsSync(CLAUDE_SETTINGS)) {
|
|
30
|
+
console.error(`Claude Code n\xE3o encontrado em ${CLAUDE_SETTINGS}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf-8"));
|
|
34
|
+
const sr = new SpacedRepetition();
|
|
35
|
+
const allTerms = buildTermIndex(loadTerms());
|
|
36
|
+
const absorbed = sr.absorbedIds();
|
|
37
|
+
const inProgress = sr.inProgressIds();
|
|
38
|
+
const inProgressTerms = allTerms.filter((t) => inProgress.has(t.id));
|
|
39
|
+
const newTerms = allTerms.filter((t) => !absorbed.has(t.id) && !inProgress.has(t.id));
|
|
40
|
+
const candidates = [...inProgressTerms, ...newTerms].slice(0, 10);
|
|
41
|
+
const verbSpinners = candidates.map((t) => `${VERBO_PREFIX}${t.term} \u2192 ${t.translation}`);
|
|
42
|
+
const existing = (settings.spinnerVerbs?.verbs ?? []).filter(
|
|
43
|
+
(v) => !v.startsWith(VERBO_PREFIX)
|
|
44
|
+
);
|
|
45
|
+
settings.spinnerVerbs = {
|
|
46
|
+
mode: settings.spinnerVerbs?.mode ?? "replace",
|
|
47
|
+
verbs: [...existing, ...verbSpinners]
|
|
48
|
+
};
|
|
49
|
+
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), "utf-8");
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(bold(" verbo instalado no Claude Code!"));
|
|
52
|
+
console.log(dim(" " + "\u2500".repeat(42)));
|
|
53
|
+
for (const v of verbSpinners) {
|
|
54
|
+
console.log(` ${green("\u2713")} ${v}`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(dim(" Esses termos aparecem enquanto o Claude pensa."));
|
|
58
|
+
console.log(dim(" Rode 'verbo install' novamente para atualizar."));
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/commands/list.ts
|
|
63
|
+
import { loadTerms as loadTerms2, SpacedRepetition as SpacedRepetition2 } from "@soltaoverbo/core";
|
|
64
|
+
var CATEGORIES = ["general", "backend", "frontend", "devops", "data", "ai"];
|
|
65
|
+
function runList(args) {
|
|
66
|
+
const catIdx = args.indexOf("--category");
|
|
67
|
+
const catFilter = catIdx !== -1 ? args[catIdx + 1]?.toLowerCase() : void 0;
|
|
68
|
+
const showAbsorbed = args.includes("--absorbed");
|
|
69
|
+
if (catFilter && !CATEGORIES.includes(catFilter)) {
|
|
70
|
+
console.error(`Categoria inv\xE1lida: "${catFilter}". V\xE1lidas: ${CATEGORIES.join(", ")}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const sr = new SpacedRepetition2();
|
|
74
|
+
const absorbed = sr.absorbedIds();
|
|
75
|
+
let terms = loadTerms2();
|
|
76
|
+
if (catFilter) terms = terms.filter((t) => t.category === catFilter);
|
|
77
|
+
if (!showAbsorbed) terms = terms.filter((t) => !absorbed.has(t.id));
|
|
78
|
+
if (terms.length === 0) {
|
|
79
|
+
const hint = showAbsorbed ? "" : ` ${dim("(use --absorbed para ver absorvidos)")}`;
|
|
80
|
+
console.log(dim(`
|
|
81
|
+
Nenhum termo encontrado.${hint}
|
|
82
|
+
`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const grouped = terms.reduce((acc, t) => {
|
|
86
|
+
;
|
|
87
|
+
(acc[t.category] ??= []).push(t);
|
|
88
|
+
return acc;
|
|
89
|
+
}, {});
|
|
90
|
+
console.log();
|
|
91
|
+
for (const cat of CATEGORIES) {
|
|
92
|
+
const group = grouped[cat];
|
|
93
|
+
if (!group) continue;
|
|
94
|
+
console.log(bold(` ${cat.toUpperCase()} (${group.length})`));
|
|
95
|
+
console.log(dim(" " + "\u2500".repeat(44)));
|
|
96
|
+
for (const term of group) {
|
|
97
|
+
const tag = absorbed.has(term.id) ? yellow(" \u2713") : "";
|
|
98
|
+
console.log(
|
|
99
|
+
` ${cyan(term.term.padEnd(22))}${tag} ${green(term.translation)}`
|
|
100
|
+
);
|
|
101
|
+
if (term.explanation) {
|
|
102
|
+
const short = term.explanation.length > 60 ? term.explanation.slice(0, 57) + "..." : term.explanation;
|
|
103
|
+
console.log(` ${"".padEnd(22)} ${dim(short)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
108
|
+
console.log(dim(` Total: ${terms.length} termos
|
|
109
|
+
`));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/commands/reset.ts
|
|
113
|
+
import { createInterface } from "readline";
|
|
114
|
+
import { SpacedRepetition as SpacedRepetition3 } from "@soltaoverbo/core";
|
|
115
|
+
async function runReset() {
|
|
116
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
117
|
+
await new Promise((resolve) => {
|
|
118
|
+
rl.question(
|
|
119
|
+
`${yellow("\u26A0")} Isso apagar\xE1 todo o hist\xF3rico de aprendizado. Confirmar? ${dim("[s/N]")} `,
|
|
120
|
+
(answer) => {
|
|
121
|
+
rl.close();
|
|
122
|
+
if (answer.trim().toLowerCase() === "s") {
|
|
123
|
+
new SpacedRepetition3().reset();
|
|
124
|
+
console.log(bold("\u2713 Hist\xF3rico resetado."));
|
|
125
|
+
} else {
|
|
126
|
+
console.log(dim("Cancelado."));
|
|
127
|
+
}
|
|
128
|
+
resolve();
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/commands/start.ts
|
|
135
|
+
import { createInterface as createInterface2 } from "readline";
|
|
136
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
137
|
+
import { extname } from "path";
|
|
138
|
+
import {
|
|
139
|
+
buildTermIndex as buildTermIndex2,
|
|
140
|
+
injectComments,
|
|
141
|
+
loadTerms as loadTerms3,
|
|
142
|
+
SpacedRepetition as SpacedRepetition4
|
|
143
|
+
} from "@soltaoverbo/core";
|
|
144
|
+
var CODE_EXTS = /* @__PURE__ */ new Set([
|
|
145
|
+
".ts",
|
|
146
|
+
".tsx",
|
|
147
|
+
".js",
|
|
148
|
+
".jsx",
|
|
149
|
+
".mjs",
|
|
150
|
+
".cjs",
|
|
151
|
+
".py",
|
|
152
|
+
".go",
|
|
153
|
+
".rs",
|
|
154
|
+
".java",
|
|
155
|
+
".kt",
|
|
156
|
+
".rb",
|
|
157
|
+
".php",
|
|
158
|
+
".cs",
|
|
159
|
+
".swift",
|
|
160
|
+
".dart",
|
|
161
|
+
".scala",
|
|
162
|
+
".ex",
|
|
163
|
+
".exs",
|
|
164
|
+
".sh",
|
|
165
|
+
".bash",
|
|
166
|
+
".zsh",
|
|
167
|
+
".fish",
|
|
168
|
+
".sql",
|
|
169
|
+
".lua",
|
|
170
|
+
".c",
|
|
171
|
+
".cpp",
|
|
172
|
+
".h",
|
|
173
|
+
".hpp"
|
|
174
|
+
]);
|
|
175
|
+
async function runStart(args) {
|
|
176
|
+
const watchIdx = args.indexOf("--watch");
|
|
177
|
+
const watchDir = watchIdx !== -1 ? args[watchIdx + 1] ?? "." : void 0;
|
|
178
|
+
const extIdx = args.indexOf("--ext");
|
|
179
|
+
const extArg = extIdx !== -1 ? args[extIdx + 1] : void 0;
|
|
180
|
+
const extensions = extArg ? new Set(extArg.split(",").map((e) => e.startsWith(".") ? e : `.${e}`)) : CODE_EXTS;
|
|
181
|
+
const sr = new SpacedRepetition4();
|
|
182
|
+
const terms = buildTermIndex2(loadTerms3());
|
|
183
|
+
if (watchDir) {
|
|
184
|
+
await watchMode(watchDir, extensions, sr, terms);
|
|
185
|
+
} else {
|
|
186
|
+
await pipeMode(sr, terms);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function pipeMode(sr, terms) {
|
|
190
|
+
const rl = createInterface2({ input: process.stdin, terminal: false });
|
|
191
|
+
const lines = [];
|
|
192
|
+
for await (const line of rl) lines.push(line);
|
|
193
|
+
const seenTerms = sr.absorbedIds();
|
|
194
|
+
const { code, injected } = injectComments(lines.join("\n"), seenTerms, terms);
|
|
195
|
+
process.stdout.write(code + "\n");
|
|
196
|
+
for (const match of injected) sr.markSeen(match.id);
|
|
197
|
+
if (injected.length > 0) {
|
|
198
|
+
process.stderr.write(dim(`
|
|
199
|
+
[verbo] ${injected.length} termo(s) anotado(s)
|
|
200
|
+
`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function watchMode(dir, extensions, sr, terms) {
|
|
204
|
+
const { default: chokidar } = await import("chokidar");
|
|
205
|
+
console.error(bold(`[verbo] Observando ${dir}`) + dim(" \u2014 Ctrl+C para parar"));
|
|
206
|
+
process.stderr.write(renderStatusLine(dir, sr));
|
|
207
|
+
const watcher = chokidar.watch(dir, {
|
|
208
|
+
ignoreInitial: true,
|
|
209
|
+
ignored: /([\\/])(\.git|node_modules|dist|\.turbo)([\\/])/,
|
|
210
|
+
persistent: true,
|
|
211
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
212
|
+
});
|
|
213
|
+
const handle = (filePath) => {
|
|
214
|
+
if (!extensions.has(extname(filePath))) return;
|
|
215
|
+
let source;
|
|
216
|
+
try {
|
|
217
|
+
source = readFileSync2(filePath, "utf-8");
|
|
218
|
+
} catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const seenTerms = sr.absorbedIds();
|
|
222
|
+
const { injected } = injectComments(source, seenTerms, terms);
|
|
223
|
+
if (injected.length > 0) {
|
|
224
|
+
process.stderr.write("\r\x1B[2K");
|
|
225
|
+
for (const match of injected) {
|
|
226
|
+
sr.markSeen(match.id);
|
|
227
|
+
logTerm(filePath, match);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
process.stderr.write(renderStatusLine(dir, sr));
|
|
231
|
+
};
|
|
232
|
+
watcher.on("add", handle).on("change", handle);
|
|
233
|
+
await new Promise((resolve) => {
|
|
234
|
+
process.on("SIGINT", () => {
|
|
235
|
+
process.stderr.write(dim("\n[verbo] Encerrando...\n"));
|
|
236
|
+
void watcher.close().then(resolve);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function renderStatusLine(dir, sr) {
|
|
241
|
+
const s = sr.stats();
|
|
242
|
+
return `\r\x1B[2K ${bold("verbo")} ${dim(dir)} ${dim("\xB7")} ${cyan(String(s.newToday))} hoje ${dim("\xB7")} ${s.total} vistos ${dim("\xB7")} ${green(String(s.absorbed))} absorvidos`;
|
|
243
|
+
}
|
|
244
|
+
function logTerm(filePath, match) {
|
|
245
|
+
const short = filePath.replace(/\\/g, "/").split("/").slice(-2).join("/");
|
|
246
|
+
process.stderr.write(
|
|
247
|
+
`${dim(short)} ${cyan(match.term)} ${dim("\u2192")} ${green(match.translation)}
|
|
248
|
+
${"".padEnd(short.length + 2)} ${dim(match.explanation)}
|
|
249
|
+
`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/commands/stats.ts
|
|
254
|
+
import { SpacedRepetition as SpacedRepetition5 } from "@soltaoverbo/core";
|
|
255
|
+
function runStats(args = []) {
|
|
256
|
+
const sr = new SpacedRepetition5();
|
|
257
|
+
const s = sr.stats();
|
|
258
|
+
if (args.includes("--short")) {
|
|
259
|
+
process.stdout.write(`verbo \xB7 ${s.newToday} hoje \xB7 ${s.total} vistos \xB7 ${s.absorbed} abs
|
|
260
|
+
`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(bold(" verbo \u2014 progresso de aprendizado"));
|
|
265
|
+
console.log(dim(" " + "\u2500".repeat(38)));
|
|
266
|
+
console.log(` ${cyan("Total visto")}: ${bold(String(s.total))}`);
|
|
267
|
+
console.log(` ${green("Absorvidos")}: ${bold(String(s.absorbed))}`);
|
|
268
|
+
console.log(` ${yellow("Em progresso")}: ${bold(String(s.inProgress))}`);
|
|
269
|
+
console.log(` ${cyan("Novos hoje")}: ${bold(String(s.newToday))}`);
|
|
270
|
+
if (s.total > 0) {
|
|
271
|
+
const pct = Math.round(s.absorbed / s.total * 100);
|
|
272
|
+
console.log();
|
|
273
|
+
console.log(` ${progressBar(pct, 32)} ${pct}% absorvido`);
|
|
274
|
+
}
|
|
275
|
+
console.log();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/bin.ts
|
|
279
|
+
if (process.platform === "win32") {
|
|
280
|
+
try {
|
|
281
|
+
execSync("chcp 65001", { stdio: "ignore" });
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
var VERSION = "0.1.0";
|
|
286
|
+
var [, , sub, ...rest] = process.argv;
|
|
287
|
+
function help() {
|
|
288
|
+
console.log(`
|
|
289
|
+
${bold("verbo")} v${VERSION} \u2014 aprenda ingl\xEAs t\xE9cnico passivamente
|
|
290
|
+
|
|
291
|
+
${bold("USO")}
|
|
292
|
+
verbo <comando> [op\xE7\xF5es]
|
|
293
|
+
|
|
294
|
+
${bold("COMANDOS")}
|
|
295
|
+
${bold("start")} Modo pipe: l\xEA stdin e injeta coment\xE1rios em pt-BR
|
|
296
|
+
${bold("start --watch")} ${dim("<dir>")} Observa diret\xF3rio e loga termos novos no terminal
|
|
297
|
+
${bold("stats")} Mostra seu progresso de aprendizado
|
|
298
|
+
${bold("stats --short")} Sa\xEDda compacta para status bars
|
|
299
|
+
${bold("install")} Injeta termos no Claude Code (barra de pensamento)
|
|
300
|
+
${bold("reset")} Limpa todo o hist\xF3rico
|
|
301
|
+
${bold("list")} Lista todos os termos dispon\xEDveis
|
|
302
|
+
${bold("list --category")} ${dim("<cat>")} Filtra por: general, backend, frontend, devops, data
|
|
303
|
+
${bold("list --absorbed")} Inclui termos j\xE1 absorvidos
|
|
304
|
+
|
|
305
|
+
${bold("EXEMPLOS")}
|
|
306
|
+
cat handler.ts | verbo start
|
|
307
|
+
verbo start --watch ./src
|
|
308
|
+
verbo stats
|
|
309
|
+
verbo install
|
|
310
|
+
verbo list --category backend
|
|
311
|
+
|
|
312
|
+
${bold("OP\xC7\xD5ES GLOBAIS")}
|
|
313
|
+
${bold("--version")}, ${bold("-v")} Vers\xE3o atual
|
|
314
|
+
${bold("--help")}, ${bold("-h")} Esta ajuda
|
|
315
|
+
`);
|
|
316
|
+
}
|
|
317
|
+
switch (sub) {
|
|
318
|
+
case "start":
|
|
319
|
+
await runStart(rest);
|
|
320
|
+
break;
|
|
321
|
+
case "stats":
|
|
322
|
+
runStats(rest);
|
|
323
|
+
break;
|
|
324
|
+
case "install":
|
|
325
|
+
runInstall();
|
|
326
|
+
break;
|
|
327
|
+
case "reset":
|
|
328
|
+
await runReset();
|
|
329
|
+
break;
|
|
330
|
+
case "list":
|
|
331
|
+
runList(rest);
|
|
332
|
+
break;
|
|
333
|
+
case "--version":
|
|
334
|
+
case "-v":
|
|
335
|
+
console.log(VERSION);
|
|
336
|
+
break;
|
|
337
|
+
case "--help":
|
|
338
|
+
case "-h":
|
|
339
|
+
case void 0:
|
|
340
|
+
help();
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
console.error(`Comando desconhecido: "${sub}". Use "verbo --help" para ver os comandos.`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soltaoverbo/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI para verbo.dev — aprenda inglês técnico passivamente",
|
|
5
|
+
"bin": {
|
|
6
|
+
"verbo": "./dist/bin.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"lint": "eslint src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@soltaoverbo/core": "workspace:*",
|
|
16
|
+
"chokidar": "^3.6.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsup": "^8.3.5",
|
|
20
|
+
"@types/node": "^22.10.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import { runInstall } from "./commands/install.ts"
|
|
3
|
+
import { runList } from "./commands/list.ts"
|
|
4
|
+
import { runReset } from "./commands/reset.ts"
|
|
5
|
+
import { runStart } from "./commands/start.ts"
|
|
6
|
+
import { runStats } from "./commands/stats.ts"
|
|
7
|
+
import { bold, dim } from "./ui.ts"
|
|
8
|
+
|
|
9
|
+
if (process.platform === "win32") {
|
|
10
|
+
try { execSync("chcp 65001", { stdio: "ignore" }) } catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const VERSION = "0.1.0"
|
|
14
|
+
const [, , sub, ...rest] = process.argv
|
|
15
|
+
|
|
16
|
+
function help(): void {
|
|
17
|
+
console.log(`
|
|
18
|
+
${bold("verbo")} v${VERSION} — aprenda inglês técnico passivamente
|
|
19
|
+
|
|
20
|
+
${bold("USO")}
|
|
21
|
+
verbo <comando> [opções]
|
|
22
|
+
|
|
23
|
+
${bold("COMANDOS")}
|
|
24
|
+
${bold("start")} Modo pipe: lê stdin e injeta comentários em pt-BR
|
|
25
|
+
${bold("start --watch")} ${dim("<dir>")} Observa diretório e loga termos novos no terminal
|
|
26
|
+
${bold("stats")} Mostra seu progresso de aprendizado
|
|
27
|
+
${bold("stats --short")} Saída compacta para status bars
|
|
28
|
+
${bold("install")} Injeta termos no Claude Code (barra de pensamento)
|
|
29
|
+
${bold("reset")} Limpa todo o histórico
|
|
30
|
+
${bold("list")} Lista todos os termos disponíveis
|
|
31
|
+
${bold("list --category")} ${dim("<cat>")} Filtra por: general, backend, frontend, devops, data
|
|
32
|
+
${bold("list --absorbed")} Inclui termos já absorvidos
|
|
33
|
+
|
|
34
|
+
${bold("EXEMPLOS")}
|
|
35
|
+
cat handler.ts | verbo start
|
|
36
|
+
verbo start --watch ./src
|
|
37
|
+
verbo stats
|
|
38
|
+
verbo install
|
|
39
|
+
verbo list --category backend
|
|
40
|
+
|
|
41
|
+
${bold("OPÇÕES GLOBAIS")}
|
|
42
|
+
${bold("--version")}, ${bold("-v")} Versão atual
|
|
43
|
+
${bold("--help")}, ${bold("-h")} Esta ajuda
|
|
44
|
+
`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (sub) {
|
|
48
|
+
case "start":
|
|
49
|
+
await runStart(rest)
|
|
50
|
+
break
|
|
51
|
+
case "stats":
|
|
52
|
+
runStats(rest)
|
|
53
|
+
break
|
|
54
|
+
case "install":
|
|
55
|
+
runInstall()
|
|
56
|
+
break
|
|
57
|
+
case "reset":
|
|
58
|
+
await runReset()
|
|
59
|
+
break
|
|
60
|
+
case "list":
|
|
61
|
+
runList(rest)
|
|
62
|
+
break
|
|
63
|
+
case "--version":
|
|
64
|
+
case "-v":
|
|
65
|
+
console.log(VERSION)
|
|
66
|
+
break
|
|
67
|
+
case "--help":
|
|
68
|
+
case "-h":
|
|
69
|
+
case undefined:
|
|
70
|
+
help()
|
|
71
|
+
break
|
|
72
|
+
default:
|
|
73
|
+
console.error(`Comando desconhecido: "${sub}". Use "verbo --help" para ver os comandos.`)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core"
|
|
5
|
+
import { bold, dim, green } from "../ui.ts"
|
|
6
|
+
|
|
7
|
+
const CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json")
|
|
8
|
+
const VERBO_PREFIX = "verbo · "
|
|
9
|
+
|
|
10
|
+
export function runInstall(): void {
|
|
11
|
+
if (!existsSync(CLAUDE_SETTINGS)) {
|
|
12
|
+
console.error(`Claude Code não encontrado em ${CLAUDE_SETTINGS}`)
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf-8"))
|
|
17
|
+
|
|
18
|
+
const sr = new SpacedRepetition()
|
|
19
|
+
const allTerms = buildTermIndex(loadTerms())
|
|
20
|
+
const absorbed = sr.absorbedIds()
|
|
21
|
+
const inProgress = sr.inProgressIds()
|
|
22
|
+
|
|
23
|
+
// Prefer terms already in progress (reforço), complete com novos se precisar
|
|
24
|
+
const inProgressTerms = allTerms.filter((t) => inProgress.has(t.id))
|
|
25
|
+
const newTerms = allTerms.filter((t) => !absorbed.has(t.id) && !inProgress.has(t.id))
|
|
26
|
+
const candidates = [...inProgressTerms, ...newTerms].slice(0, 10)
|
|
27
|
+
|
|
28
|
+
const verbSpinners = candidates.map((t) => `${VERBO_PREFIX}${t.term} → ${t.translation}`)
|
|
29
|
+
|
|
30
|
+
const existing: string[] = (settings.spinnerVerbs?.verbs ?? []).filter(
|
|
31
|
+
(v: string) => !v.startsWith(VERBO_PREFIX),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
settings.spinnerVerbs = {
|
|
35
|
+
mode: settings.spinnerVerbs?.mode ?? "replace",
|
|
36
|
+
verbs: [...existing, ...verbSpinners],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), "utf-8")
|
|
40
|
+
|
|
41
|
+
console.log()
|
|
42
|
+
console.log(bold(" verbo instalado no Claude Code!"))
|
|
43
|
+
console.log(dim(" " + "─".repeat(42)))
|
|
44
|
+
for (const v of verbSpinners) {
|
|
45
|
+
console.log(` ${green("✓")} ${v}`)
|
|
46
|
+
}
|
|
47
|
+
console.log()
|
|
48
|
+
console.log(dim(" Esses termos aparecem enquanto o Claude pensa."))
|
|
49
|
+
console.log(dim(" Rode 'verbo install' novamente para atualizar."))
|
|
50
|
+
console.log()
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadTerms, SpacedRepetition } from "@soltaoverbo/core"
|
|
2
|
+
import { bold, cyan, dim, green, yellow } from "../ui.ts"
|
|
3
|
+
|
|
4
|
+
const CATEGORIES = ["general", "backend", "frontend", "devops", "data", "ai"]
|
|
5
|
+
|
|
6
|
+
export function runList(args: string[]): void {
|
|
7
|
+
const catIdx = args.indexOf("--category")
|
|
8
|
+
const catFilter = catIdx !== -1 ? args[catIdx + 1]?.toLowerCase() : undefined
|
|
9
|
+
const showAbsorbed = args.includes("--absorbed")
|
|
10
|
+
|
|
11
|
+
if (catFilter && !CATEGORIES.includes(catFilter)) {
|
|
12
|
+
console.error(`Categoria inválida: "${catFilter}". Válidas: ${CATEGORIES.join(", ")}`)
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sr = new SpacedRepetition()
|
|
17
|
+
const absorbed = sr.absorbedIds()
|
|
18
|
+
|
|
19
|
+
let terms = loadTerms()
|
|
20
|
+
if (catFilter) terms = terms.filter((t) => t.category === catFilter)
|
|
21
|
+
if (!showAbsorbed) terms = terms.filter((t) => !absorbed.has(t.id))
|
|
22
|
+
|
|
23
|
+
if (terms.length === 0) {
|
|
24
|
+
const hint = showAbsorbed ? "" : ` ${dim("(use --absorbed para ver absorvidos)")}`
|
|
25
|
+
console.log(dim(`\n Nenhum termo encontrado.${hint}\n`))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// group by category (Node 20 compat — no Map.groupBy)
|
|
30
|
+
const grouped = terms.reduce<Record<string, typeof terms>>((acc, t) => {
|
|
31
|
+
;(acc[t.category] ??= []).push(t)
|
|
32
|
+
return acc
|
|
33
|
+
}, {})
|
|
34
|
+
|
|
35
|
+
console.log()
|
|
36
|
+
for (const cat of CATEGORIES) {
|
|
37
|
+
const group = grouped[cat]
|
|
38
|
+
if (!group) continue
|
|
39
|
+
|
|
40
|
+
console.log(bold(` ${cat.toUpperCase()} (${group.length})`))
|
|
41
|
+
console.log(dim(" " + "─".repeat(44)))
|
|
42
|
+
|
|
43
|
+
for (const term of group) {
|
|
44
|
+
const tag = absorbed.has(term.id) ? yellow(" ✓") : ""
|
|
45
|
+
console.log(
|
|
46
|
+
` ${cyan(term.term.padEnd(22))}${tag} ${green(term.translation)}`,
|
|
47
|
+
)
|
|
48
|
+
if (term.explanation) {
|
|
49
|
+
const short = term.explanation.length > 60
|
|
50
|
+
? term.explanation.slice(0, 57) + "..."
|
|
51
|
+
: term.explanation
|
|
52
|
+
console.log(` ${"".padEnd(22)} ${dim(short)}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(dim(` Total: ${terms.length} termos\n`))
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createInterface } from "node:readline"
|
|
2
|
+
import { SpacedRepetition } from "@soltaoverbo/core"
|
|
3
|
+
import { bold, dim, yellow } from "../ui.ts"
|
|
4
|
+
|
|
5
|
+
export async function runReset(): Promise<void> {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
7
|
+
|
|
8
|
+
await new Promise<void>((resolve) => {
|
|
9
|
+
rl.question(
|
|
10
|
+
`${yellow("⚠")} Isso apagará todo o histórico de aprendizado. Confirmar? ${dim("[s/N]")} `,
|
|
11
|
+
(answer) => {
|
|
12
|
+
rl.close()
|
|
13
|
+
if (answer.trim().toLowerCase() === "s") {
|
|
14
|
+
new SpacedRepetition().reset()
|
|
15
|
+
console.log(bold("✓ Histórico resetado."))
|
|
16
|
+
} else {
|
|
17
|
+
console.log(dim("Cancelado."))
|
|
18
|
+
}
|
|
19
|
+
resolve()
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createInterface } from "node:readline"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { extname } from "node:path"
|
|
4
|
+
import {
|
|
5
|
+
buildTermIndex,
|
|
6
|
+
injectComments,
|
|
7
|
+
loadTerms,
|
|
8
|
+
SpacedRepetition,
|
|
9
|
+
type TermMatch,
|
|
10
|
+
} from "@soltaoverbo/core"
|
|
11
|
+
import { bold, cyan, dim, green } from "../ui.ts"
|
|
12
|
+
|
|
13
|
+
const CODE_EXTS = new Set([
|
|
14
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
15
|
+
".py", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".cs",
|
|
16
|
+
".swift", ".dart", ".scala", ".ex", ".exs",
|
|
17
|
+
".sh", ".bash", ".zsh", ".fish",
|
|
18
|
+
".sql", ".lua", ".c", ".cpp", ".h", ".hpp",
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export async function runStart(args: string[]): Promise<void> {
|
|
22
|
+
const watchIdx = args.indexOf("--watch")
|
|
23
|
+
const watchDir = watchIdx !== -1 ? (args[watchIdx + 1] ?? ".") : undefined
|
|
24
|
+
const extIdx = args.indexOf("--ext")
|
|
25
|
+
const extArg = extIdx !== -1 ? args[extIdx + 1] : undefined
|
|
26
|
+
const extensions = extArg
|
|
27
|
+
? new Set(extArg.split(",").map((e) => (e.startsWith(".") ? e : `.${e}`)))
|
|
28
|
+
: CODE_EXTS
|
|
29
|
+
|
|
30
|
+
const sr = new SpacedRepetition()
|
|
31
|
+
const terms = buildTermIndex(loadTerms())
|
|
32
|
+
|
|
33
|
+
if (watchDir) {
|
|
34
|
+
await watchMode(watchDir, extensions, sr, terms)
|
|
35
|
+
} else {
|
|
36
|
+
await pipeMode(sr, terms)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Pipe mode ──────────────────────────────────────────────────────────────
|
|
41
|
+
// Usage: cat file.ts | verbo start
|
|
42
|
+
// Reads stdin, injects pt-BR comments, writes to stdout.
|
|
43
|
+
|
|
44
|
+
async function pipeMode(
|
|
45
|
+
sr: SpacedRepetition,
|
|
46
|
+
terms: ReturnType<typeof buildTermIndex>,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
49
|
+
const lines: string[] = []
|
|
50
|
+
for await (const line of rl) lines.push(line)
|
|
51
|
+
|
|
52
|
+
const seenTerms = sr.absorbedIds()
|
|
53
|
+
const { code, injected } = injectComments(lines.join("\n"), seenTerms, terms)
|
|
54
|
+
|
|
55
|
+
process.stdout.write(code + "\n")
|
|
56
|
+
|
|
57
|
+
for (const match of injected) sr.markSeen(match.id)
|
|
58
|
+
|
|
59
|
+
if (injected.length > 0) {
|
|
60
|
+
process.stderr.write(dim(`\n[verbo] ${injected.length} termo(s) anotado(s)\n`))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Watch mode ──────────────────────────────────────────────────────────────
|
|
65
|
+
// Usage: verbo start --watch ./src
|
|
66
|
+
// Watches a directory; logs newly detected terms to stderr without touching files.
|
|
67
|
+
|
|
68
|
+
async function watchMode(
|
|
69
|
+
dir: string,
|
|
70
|
+
extensions: Set<string>,
|
|
71
|
+
sr: SpacedRepetition,
|
|
72
|
+
terms: ReturnType<typeof buildTermIndex>,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const { default: chokidar } = await import("chokidar")
|
|
75
|
+
|
|
76
|
+
console.error(bold(`[verbo] Observando ${dir}`) + dim(" — Ctrl+C para parar"))
|
|
77
|
+
process.stderr.write(renderStatusLine(dir, sr))
|
|
78
|
+
|
|
79
|
+
const watcher = chokidar.watch(dir, {
|
|
80
|
+
ignoreInitial: true,
|
|
81
|
+
ignored: /([\\/])(\.git|node_modules|dist|\.turbo)([\\/])/,
|
|
82
|
+
persistent: true,
|
|
83
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const handle = (filePath: string) => {
|
|
87
|
+
if (!extensions.has(extname(filePath))) return
|
|
88
|
+
let source: string
|
|
89
|
+
try { source = readFileSync(filePath, "utf-8") } catch { return }
|
|
90
|
+
|
|
91
|
+
const seenTerms = sr.absorbedIds()
|
|
92
|
+
const { injected } = injectComments(source, seenTerms, terms)
|
|
93
|
+
|
|
94
|
+
if (injected.length > 0) {
|
|
95
|
+
process.stderr.write("\r\x1b[2K")
|
|
96
|
+
for (const match of injected) {
|
|
97
|
+
sr.markSeen(match.id)
|
|
98
|
+
logTerm(filePath, match)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.stderr.write(renderStatusLine(dir, sr))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
watcher.on("add", handle).on("change", handle)
|
|
106
|
+
|
|
107
|
+
await new Promise<void>((resolve) => {
|
|
108
|
+
process.on("SIGINT", () => {
|
|
109
|
+
process.stderr.write(dim("\n[verbo] Encerrando...\n"))
|
|
110
|
+
void watcher.close().then(resolve)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderStatusLine(dir: string, sr: SpacedRepetition): string {
|
|
116
|
+
const s = sr.stats()
|
|
117
|
+
return (
|
|
118
|
+
`\r\x1b[2K` +
|
|
119
|
+
` ${bold("verbo")} ${dim(dir)} ${dim("·")}` +
|
|
120
|
+
` ${cyan(String(s.newToday))} hoje` +
|
|
121
|
+
` ${dim("·")} ${s.total} vistos` +
|
|
122
|
+
` ${dim("·")} ${green(String(s.absorbed))} absorvidos`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function logTerm(filePath: string, match: TermMatch): void {
|
|
127
|
+
// Show only last 2 path segments to keep lines short
|
|
128
|
+
const short = filePath.replace(/\\/g, "/").split("/").slice(-2).join("/")
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
`${dim(short)} ${cyan(match.term)} ${dim("→")} ${green(match.translation)}\n` +
|
|
131
|
+
`${"".padEnd(short.length + 2)} ${dim(match.explanation)}\n`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SpacedRepetition } from "@soltaoverbo/core"
|
|
2
|
+
import { bold, cyan, dim, green, progressBar, yellow } from "../ui.ts"
|
|
3
|
+
|
|
4
|
+
export function runStats(args: string[] = []): void {
|
|
5
|
+
const sr = new SpacedRepetition()
|
|
6
|
+
const s = sr.stats()
|
|
7
|
+
|
|
8
|
+
if (args.includes("--short")) {
|
|
9
|
+
process.stdout.write(`verbo · ${s.newToday} hoje · ${s.total} vistos · ${s.absorbed} abs\n`)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log()
|
|
14
|
+
console.log(bold(" verbo — progresso de aprendizado"))
|
|
15
|
+
console.log(dim(" " + "─".repeat(38)))
|
|
16
|
+
console.log(` ${cyan("Total visto")}: ${bold(String(s.total))}`)
|
|
17
|
+
console.log(` ${green("Absorvidos")}: ${bold(String(s.absorbed))}`)
|
|
18
|
+
console.log(` ${yellow("Em progresso")}: ${bold(String(s.inProgress))}`)
|
|
19
|
+
console.log(` ${cyan("Novos hoje")}: ${bold(String(s.newToday))}`)
|
|
20
|
+
|
|
21
|
+
if (s.total > 0) {
|
|
22
|
+
const pct = Math.round((s.absorbed / s.total) * 100)
|
|
23
|
+
console.log()
|
|
24
|
+
console.log(` ${progressBar(pct, 32)} ${pct}% absorvido`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log()
|
|
28
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ANSI helpers — thin wrappers so we never need chalk
|
|
2
|
+
const R = "\x1b[0m"
|
|
3
|
+
|
|
4
|
+
export const bold = (s: string) => `\x1b[1m${s}${R}`
|
|
5
|
+
export const dim = (s: string) => `\x1b[2m${s}${R}`
|
|
6
|
+
export const green = (s: string) => `\x1b[32m${s}${R}`
|
|
7
|
+
export const yellow = (s: string) => `\x1b[33m${s}${R}`
|
|
8
|
+
export const cyan = (s: string) => `\x1b[36m${s}${R}`
|
|
9
|
+
|
|
10
|
+
export function progressBar(pct: number, width: number): string {
|
|
11
|
+
const filled = Math.round((pct / 100) * width)
|
|
12
|
+
const empty = width - filled
|
|
13
|
+
return `\x1b[32m${"█".repeat(filled)}\x1b[2m${"░".repeat(empty)}${R}`
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "tsup"
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/bin.ts"],
|
|
5
|
+
format: ["esm"],
|
|
6
|
+
target: "node20",
|
|
7
|
+
outDir: "dist",
|
|
8
|
+
clean: true,
|
|
9
|
+
dts: false,
|
|
10
|
+
// shebang injected by tsup banner so chmod +x works after install
|
|
11
|
+
banner: { js: "#!/usr/bin/env node" },
|
|
12
|
+
external: ["chokidar", "@soltaoverbo/core"],
|
|
13
|
+
})
|