@soltaoverbo/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +116 -15
- package/package.json +2 -2
- package/src/bin.ts +13 -1
- package/src/commands/config.ts +35 -0
- package/src/commands/explain.ts +62 -0
package/dist/bin.js
CHANGED
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
// src/bin.ts
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
7
|
-
import {
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core";
|
|
6
|
+
// src/commands/config.ts
|
|
7
|
+
import { getApiKey, setApiKey } from "@soltaoverbo/core";
|
|
11
8
|
|
|
12
9
|
// src/ui.ts
|
|
13
10
|
var R = "\x1B[0m";
|
|
@@ -22,7 +19,101 @@ function progressBar(pct, width) {
|
|
|
22
19
|
return `\x1B[32m${"█".repeat(filled)}\x1B[2m${"░".repeat(empty)}${R}`;
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
// src/commands/config.ts
|
|
23
|
+
function runConfig(args) {
|
|
24
|
+
const [subcmd, ...rest2] = args;
|
|
25
|
+
switch (subcmd) {
|
|
26
|
+
case "set-key": {
|
|
27
|
+
const key = rest2[0];
|
|
28
|
+
if (!key) {
|
|
29
|
+
console.error(`${bold("Uso:")} verbo config set-key <chave>`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
setApiKey(key);
|
|
33
|
+
console.log(`${bold("[verbo]")} API key salva em ${dim("~/.verbo/config.json")}`);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "show-key": {
|
|
37
|
+
const key = getApiKey();
|
|
38
|
+
if (!key) {
|
|
39
|
+
console.log(dim("Nenhuma API key configurada."));
|
|
40
|
+
} else {
|
|
41
|
+
console.log(`API key: ${cyan(`${key.slice(0, 8)}...${key.slice(-4)}`)}`);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
console.log(
|
|
47
|
+
`${bold("verbo config")} — configurações do verbo
|
|
48
|
+
|
|
49
|
+
${bold("set-key")} ${dim("<chave>")} Salva a API key da Anthropic em ~/.verbo/config.json
|
|
50
|
+
${bold("show-key")} Exibe a API key atual (parcial)
|
|
51
|
+
`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/commands/explain.ts
|
|
57
|
+
import { createInterface } from "readline";
|
|
58
|
+
import { readFileSync } from "fs";
|
|
59
|
+
import { basename, extname } from "path";
|
|
60
|
+
import { explainCode, getApiKey as getApiKey2, hasConsented, injectExplanations, setConsented } from "@soltaoverbo/core";
|
|
61
|
+
async function runExplain(args) {
|
|
62
|
+
const filePath = args[0];
|
|
63
|
+
if (!filePath) {
|
|
64
|
+
console.error(`${bold("Uso:")} verbo explain <arquivo>`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const apiKey = getApiKey2();
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
console.error(
|
|
70
|
+
`${bold("verbo explain")} requer uma API key da Anthropic.
|
|
71
|
+
Configure com: ${cyan("verbo config set-key <sua-chave>")}
|
|
72
|
+
Obtenha sua chave em: https://console.anthropic.com/`
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (!hasConsented()) {
|
|
77
|
+
const confirmed = await askConsent(basename(filePath));
|
|
78
|
+
if (!confirmed) {
|
|
79
|
+
console.error("Operação cancelada.");
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
setConsented();
|
|
83
|
+
}
|
|
84
|
+
let code;
|
|
85
|
+
try {
|
|
86
|
+
code = readFileSync(filePath, "utf-8");
|
|
87
|
+
} catch {
|
|
88
|
+
console.error(`Não foi possível ler o arquivo: ${filePath}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const lang = extname(filePath).replace(".", "") || "código";
|
|
92
|
+
process.stderr.write(dim(`[verbo] Explicando ${basename(filePath)}...
|
|
93
|
+
`));
|
|
94
|
+
const explanations = await explainCode(code, lang, apiKey);
|
|
95
|
+
const annotated = injectExplanations(code, explanations);
|
|
96
|
+
process.stdout.write(annotated + "\n");
|
|
97
|
+
}
|
|
98
|
+
function askConsent(fileName) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
101
|
+
rl.question(
|
|
102
|
+
`${bold("[verbo]")} O conteúdo de ${cyan(fileName)} será enviado para a API da Anthropic.
|
|
103
|
+
Esta mensagem só aparece uma vez. Continuar? (s/N): `,
|
|
104
|
+
(answer) => {
|
|
105
|
+
rl.close();
|
|
106
|
+
resolve(answer.trim().toLowerCase() === "s");
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
25
112
|
// src/commands/install.ts
|
|
113
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
114
|
+
import { homedir } from "os";
|
|
115
|
+
import { join } from "path";
|
|
116
|
+
import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core";
|
|
26
117
|
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
27
118
|
var VERBO_PREFIX = "verbo · ";
|
|
28
119
|
function runInstall() {
|
|
@@ -30,7 +121,7 @@ function runInstall() {
|
|
|
30
121
|
console.error(`Claude Code não encontrado em ${CLAUDE_SETTINGS}`);
|
|
31
122
|
process.exit(1);
|
|
32
123
|
}
|
|
33
|
-
const settings = JSON.parse(
|
|
124
|
+
const settings = JSON.parse(readFileSync2(CLAUDE_SETTINGS, "utf-8"));
|
|
34
125
|
const sr = new SpacedRepetition();
|
|
35
126
|
const allTerms = buildTermIndex(loadTerms());
|
|
36
127
|
const absorbed = sr.absorbedIds();
|
|
@@ -110,10 +201,10 @@ function runList(args) {
|
|
|
110
201
|
}
|
|
111
202
|
|
|
112
203
|
// src/commands/reset.ts
|
|
113
|
-
import { createInterface } from "readline";
|
|
204
|
+
import { createInterface as createInterface2 } from "readline";
|
|
114
205
|
import { SpacedRepetition as SpacedRepetition3 } from "@soltaoverbo/core";
|
|
115
206
|
async function runReset() {
|
|
116
|
-
const rl =
|
|
207
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
117
208
|
await new Promise((resolve) => {
|
|
118
209
|
rl.question(
|
|
119
210
|
`${yellow("⚠")} Isso apagará todo o histórico de aprendizado. Confirmar? ${dim("[s/N]")} `,
|
|
@@ -132,9 +223,9 @@ async function runReset() {
|
|
|
132
223
|
}
|
|
133
224
|
|
|
134
225
|
// src/commands/start.ts
|
|
135
|
-
import { createInterface as
|
|
136
|
-
import { readFileSync as
|
|
137
|
-
import { extname } from "path";
|
|
226
|
+
import { createInterface as createInterface3 } from "readline";
|
|
227
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
228
|
+
import { extname as extname2 } from "path";
|
|
138
229
|
import {
|
|
139
230
|
buildTermIndex as buildTermIndex2,
|
|
140
231
|
injectComments,
|
|
@@ -187,7 +278,7 @@ async function runStart(args) {
|
|
|
187
278
|
}
|
|
188
279
|
}
|
|
189
280
|
async function pipeMode(sr, terms) {
|
|
190
|
-
const rl =
|
|
281
|
+
const rl = createInterface3({ input: process.stdin, terminal: false });
|
|
191
282
|
const lines = [];
|
|
192
283
|
for await (const line of rl) lines.push(line);
|
|
193
284
|
const seenTerms = sr.absorbedIds();
|
|
@@ -211,10 +302,10 @@ async function watchMode(dir, extensions, sr, terms) {
|
|
|
211
302
|
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
212
303
|
});
|
|
213
304
|
const handle = (filePath) => {
|
|
214
|
-
if (!extensions.has(
|
|
305
|
+
if (!extensions.has(extname2(filePath))) return;
|
|
215
306
|
let source;
|
|
216
307
|
try {
|
|
217
|
-
source =
|
|
308
|
+
source = readFileSync3(filePath, "utf-8");
|
|
218
309
|
} catch {
|
|
219
310
|
return;
|
|
220
311
|
}
|
|
@@ -289,7 +380,7 @@ if (process.platform === "win32") {
|
|
|
289
380
|
}
|
|
290
381
|
}
|
|
291
382
|
}
|
|
292
|
-
var VERSION = "0.
|
|
383
|
+
var VERSION = "0.4.1";
|
|
293
384
|
var [, , sub, ...rest] = process.argv;
|
|
294
385
|
function help() {
|
|
295
386
|
console.log(`
|
|
@@ -301,6 +392,8 @@ function help() {
|
|
|
301
392
|
${bold("COMANDOS")}
|
|
302
393
|
${bold("start")} Modo pipe: lê stdin e injeta comentários em pt-BR
|
|
303
394
|
${bold("start --watch")} ${dim("<dir>")} Observa diretório e loga termos novos no terminal
|
|
395
|
+
${bold("explain")} ${dim("<arquivo>")} Explica o código linha por linha via IA (pt-BR)
|
|
396
|
+
${bold("config set-key")} ${dim("<chave>")} Salva a API key da Anthropic
|
|
304
397
|
${bold("stats")} Mostra seu progresso de aprendizado
|
|
305
398
|
${bold("stats --short")} Saída compacta para status bars
|
|
306
399
|
${bold("install")} Injeta termos no Claude Code (barra de pensamento)
|
|
@@ -312,6 +405,8 @@ function help() {
|
|
|
312
405
|
${bold("EXEMPLOS")}
|
|
313
406
|
cat handler.ts | verbo start
|
|
314
407
|
verbo start --watch ./src
|
|
408
|
+
verbo explain handler.ts
|
|
409
|
+
verbo config set-key sk-ant-...
|
|
315
410
|
verbo stats
|
|
316
411
|
verbo install
|
|
317
412
|
verbo list --category backend
|
|
@@ -325,6 +420,12 @@ switch (sub) {
|
|
|
325
420
|
case "start":
|
|
326
421
|
await runStart(rest);
|
|
327
422
|
break;
|
|
423
|
+
case "explain":
|
|
424
|
+
await runExplain(rest);
|
|
425
|
+
break;
|
|
426
|
+
case "config":
|
|
427
|
+
runConfig(rest);
|
|
428
|
+
break;
|
|
328
429
|
case "stats":
|
|
329
430
|
runStats(rest);
|
|
330
431
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soltaoverbo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI para verbo.dev — aprenda inglês técnico passivamente",
|
|
5
5
|
"bin": {
|
|
6
6
|
"verbo": "./dist/bin.js"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"chokidar": "^3.6.0",
|
|
11
|
-
"@soltaoverbo/core": "0.
|
|
11
|
+
"@soltaoverbo/core": "0.4.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"tsup": "^8.3.5",
|
package/src/bin.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process"
|
|
2
|
+
import { runConfig } from "./commands/config.ts"
|
|
3
|
+
import { runExplain } from "./commands/explain.ts"
|
|
2
4
|
import { runInstall } from "./commands/install.ts"
|
|
3
5
|
import { runList } from "./commands/list.ts"
|
|
4
6
|
import { runReset } from "./commands/reset.ts"
|
|
@@ -21,7 +23,7 @@ if (process.platform === "win32") {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const VERSION = "0.
|
|
26
|
+
const VERSION = "0.4.1"
|
|
25
27
|
const [, , sub, ...rest] = process.argv
|
|
26
28
|
|
|
27
29
|
function help(): void {
|
|
@@ -34,6 +36,8 @@ function help(): void {
|
|
|
34
36
|
${bold("COMANDOS")}
|
|
35
37
|
${bold("start")} Modo pipe: lê stdin e injeta comentários em pt-BR
|
|
36
38
|
${bold("start --watch")} ${dim("<dir>")} Observa diretório e loga termos novos no terminal
|
|
39
|
+
${bold("explain")} ${dim("<arquivo>")} Explica o código linha por linha via IA (pt-BR)
|
|
40
|
+
${bold("config set-key")} ${dim("<chave>")} Salva a API key da Anthropic
|
|
37
41
|
${bold("stats")} Mostra seu progresso de aprendizado
|
|
38
42
|
${bold("stats --short")} Saída compacta para status bars
|
|
39
43
|
${bold("install")} Injeta termos no Claude Code (barra de pensamento)
|
|
@@ -45,6 +49,8 @@ function help(): void {
|
|
|
45
49
|
${bold("EXEMPLOS")}
|
|
46
50
|
cat handler.ts | verbo start
|
|
47
51
|
verbo start --watch ./src
|
|
52
|
+
verbo explain handler.ts
|
|
53
|
+
verbo config set-key sk-ant-...
|
|
48
54
|
verbo stats
|
|
49
55
|
verbo install
|
|
50
56
|
verbo list --category backend
|
|
@@ -59,6 +65,12 @@ switch (sub) {
|
|
|
59
65
|
case "start":
|
|
60
66
|
await runStart(rest)
|
|
61
67
|
break
|
|
68
|
+
case "explain":
|
|
69
|
+
await runExplain(rest)
|
|
70
|
+
break
|
|
71
|
+
case "config":
|
|
72
|
+
runConfig(rest)
|
|
73
|
+
break
|
|
62
74
|
case "stats":
|
|
63
75
|
runStats(rest)
|
|
64
76
|
break
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getApiKey, setApiKey } from "@soltaoverbo/core"
|
|
2
|
+
import { bold, cyan, dim } from "../ui.ts"
|
|
3
|
+
|
|
4
|
+
export function runConfig(args: string[]): void {
|
|
5
|
+
const [subcmd, ...rest] = args
|
|
6
|
+
|
|
7
|
+
switch (subcmd) {
|
|
8
|
+
case "set-key": {
|
|
9
|
+
const key = rest[0]
|
|
10
|
+
if (!key) {
|
|
11
|
+
console.error(`${bold("Uso:")} verbo config set-key <chave>`)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
setApiKey(key)
|
|
15
|
+
console.log(`${bold("[verbo]")} API key salva em ${dim("~/.verbo/config.json")}`)
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
case "show-key": {
|
|
19
|
+
const key = getApiKey()
|
|
20
|
+
if (!key) {
|
|
21
|
+
console.log(dim("Nenhuma API key configurada."))
|
|
22
|
+
} else {
|
|
23
|
+
// Show only first 8 and last 4 chars — never log the full key
|
|
24
|
+
console.log(`API key: ${cyan(`${key.slice(0, 8)}...${key.slice(-4)}`)}`)
|
|
25
|
+
}
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
default:
|
|
29
|
+
console.log(
|
|
30
|
+
`${bold("verbo config")} — configurações do verbo\n\n` +
|
|
31
|
+
` ${bold("set-key")} ${dim("<chave>")} Salva a API key da Anthropic em ~/.verbo/config.json\n` +
|
|
32
|
+
` ${bold("show-key")} Exibe a API key atual (parcial)\n`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createInterface } from "node:readline"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { basename, extname } from "node:path"
|
|
4
|
+
import { explainCode, getApiKey, hasConsented, injectExplanations, setConsented } from "@soltaoverbo/core"
|
|
5
|
+
import { bold, cyan, dim } from "../ui.ts"
|
|
6
|
+
|
|
7
|
+
export async function runExplain(args: string[]): Promise<void> {
|
|
8
|
+
const filePath = args[0]
|
|
9
|
+
if (!filePath) {
|
|
10
|
+
console.error(`${bold("Uso:")} verbo explain <arquivo>`)
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const apiKey = getApiKey()
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
console.error(
|
|
17
|
+
`${bold("verbo explain")} requer uma API key da Anthropic.\n` +
|
|
18
|
+
`Configure com: ${cyan("verbo config set-key <sua-chave>")}\n` +
|
|
19
|
+
`Obtenha sua chave em: https://console.anthropic.com/`,
|
|
20
|
+
)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!hasConsented()) {
|
|
25
|
+
const confirmed = await askConsent(basename(filePath))
|
|
26
|
+
if (!confirmed) {
|
|
27
|
+
console.error("Operação cancelada.")
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
setConsented()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let code: string
|
|
34
|
+
try {
|
|
35
|
+
code = readFileSync(filePath, "utf-8")
|
|
36
|
+
} catch {
|
|
37
|
+
console.error(`Não foi possível ler o arquivo: ${filePath}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lang = extname(filePath).replace(".", "") || "código"
|
|
42
|
+
|
|
43
|
+
process.stderr.write(dim(`[verbo] Explicando ${basename(filePath)}...\n`))
|
|
44
|
+
|
|
45
|
+
const explanations = await explainCode(code, lang, apiKey)
|
|
46
|
+
const annotated = injectExplanations(code, explanations)
|
|
47
|
+
process.stdout.write(annotated + "\n")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function askConsent(fileName: string): Promise<boolean> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
53
|
+
rl.question(
|
|
54
|
+
`${bold("[verbo]")} O conteúdo de ${cyan(fileName)} será enviado para a API da Anthropic.\n` +
|
|
55
|
+
` Esta mensagem só aparece uma vez. Continuar? (s/N): `,
|
|
56
|
+
(answer) => {
|
|
57
|
+
rl.close()
|
|
58
|
+
resolve(answer.trim().toLowerCase() === "s")
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
}
|