@memoring/cli 0.2.0 → 0.3.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/README.md +12 -0
- package/dist/commands/update.js +35 -0
- package/dist/config.js +41 -4
- package/dist/main.js +25 -2
- package/dist/update.js +126 -0
- package/package.json +1 -1
- package/skills/memoring-cli/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -10,6 +10,16 @@ A CLI não tem LLM própria: quem lê o material e gera os flashcards é o seu a
|
|
|
10
10
|
npm install -g @memoring/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Atualização
|
|
14
|
+
|
|
15
|
+
A CLI verifica em background (no máximo 1x a cada 24h) se há versão nova no npm e avisa no stderr — nunca bloqueia o comando nem polui a saída `--json`. Para atualizar:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
memoring update # equivale a npm i -g @memoring/cli@latest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`MEMORING_NO_UPDATE_CHECK=1` desativa a verificação.
|
|
22
|
+
|
|
13
23
|
## Quickstart
|
|
14
24
|
|
|
15
25
|
Autentique (abre o browser — sem senha no terminal):
|
|
@@ -46,6 +56,7 @@ Os itens enviados nascem **PENDING_REVIEW** — nada entra nas suas listagens ne
|
|
|
46
56
|
| `memoring auth` | login / logout / status |
|
|
47
57
|
| `memoring stage` | schema (o contrato) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes, `--max-notes\|--max-flashcards\|--max-one-shots` limitam cada tipo) |
|
|
48
58
|
| `memoring inbox` | list / accept `<id>` / accept-all / discard `<id>` |
|
|
59
|
+
| `memoring update` | atualiza a CLI para a última versão (via npm) |
|
|
49
60
|
| `memoring version` | versão da CLI |
|
|
50
61
|
|
|
51
62
|
## Flags
|
|
@@ -95,5 +106,6 @@ memoring auth login
|
|
|
95
106
|
```
|
|
96
107
|
|
|
97
108
|
Variáveis: `MEMORING_API_BASE`, `MEMORING_KEYCLOAK_URL`, `MEMORING_KC_REALM`, `MEMORING_KC_CLIENT`, `MEMORING_CONFIG_HOME` (isola o config.json).
|
|
109
|
+
Override de ambiente vale por sessão: o `auth login` não o grava no config.json — exporte as variáveis em cada shell (ou use `memoring --api-base ...` por comando).
|
|
98
110
|
|
|
99
111
|
Build a partir do repo: `cd cli && npm install && npm run build && node dist/main.js --help`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { bold, dim, fail, green, yellow } from './shared.js';
|
|
3
|
+
import { fetchLatestVersion, isNewerVersion, recordUpdateCheck } from '../update.js';
|
|
4
|
+
const PACKAGE = '@memoring/cli';
|
|
5
|
+
export async function updateCommand(current) {
|
|
6
|
+
process.stdout.write(dim('consultando o registry npm...') + '\n');
|
|
7
|
+
const latest = await fetchLatestVersion();
|
|
8
|
+
if (latest && !isNewerVersion(latest, current)) {
|
|
9
|
+
recordUpdateCheck(latest);
|
|
10
|
+
process.stdout.write(green(`o memoring já está na versão mais recente (${current}).`) + '\n');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (latest) {
|
|
14
|
+
process.stdout.write(`atualizando ${current} → ${bold(latest)}...\n`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
process.stdout.write(yellow('registry indisponível — tentando atualizar via npm mesmo assim...') + '\n');
|
|
18
|
+
}
|
|
19
|
+
const code = await runNpmGlobalInstall();
|
|
20
|
+
if (code !== 0) {
|
|
21
|
+
fail(`a atualização via npm falhou (código ${code}). Tente manualmente: npm install -g ${PACKAGE}@latest`);
|
|
22
|
+
}
|
|
23
|
+
recordUpdateCheck(latest ?? undefined);
|
|
24
|
+
process.stdout.write(green('memoring atualizado — rode `memoring version` para conferir.') + '\n');
|
|
25
|
+
}
|
|
26
|
+
function runNpmGlobalInstall() {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const child = spawn('npm', ['install', '-g', `${PACKAGE}@latest`], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
shell: process.platform === 'win32',
|
|
31
|
+
});
|
|
32
|
+
child.on('error', () => resolve(-1));
|
|
33
|
+
child.on('close', (code) => resolve(code ?? -1));
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const DEFAULTS = {
|
|
@@ -7,7 +7,7 @@ const DEFAULTS = {
|
|
|
7
7
|
clientId: 'memoring-cli',
|
|
8
8
|
apiBase: 'https://memoring.com.br/api',
|
|
9
9
|
};
|
|
10
|
-
function configDir() {
|
|
10
|
+
export function configDir() {
|
|
11
11
|
if (process.env.MEMORING_CONFIG_HOME) {
|
|
12
12
|
return process.env.MEMORING_CONFIG_HOME;
|
|
13
13
|
}
|
|
@@ -41,10 +41,47 @@ export function loadConfig() {
|
|
|
41
41
|
email: stored.email,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Endpoints para PERSISTIR: só o que veio do arquivo ou do default. Override de
|
|
46
|
+
* ambiente (MEMORING_API_BASE etc.) é efêmero por definição — gravá-lo no
|
|
47
|
+
* config.json faria comandos futuros, sem a variável, apontarem para o
|
|
48
|
+
* ambiente errado (ex.: dev).
|
|
49
|
+
*/
|
|
50
|
+
export function storedEndpoints() {
|
|
51
|
+
const stored = readStored();
|
|
52
|
+
return {
|
|
53
|
+
keycloakUrl: stored.keycloakUrl ?? DEFAULTS.keycloakUrl,
|
|
54
|
+
realm: stored.realm ?? DEFAULTS.realm,
|
|
55
|
+
clientId: stored.clientId ?? DEFAULTS.clientId,
|
|
56
|
+
apiBase: stored.apiBase ?? DEFAULTS.apiBase,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function writeAtomic(json) {
|
|
60
|
+
const path = configPath();
|
|
61
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
62
|
+
writeFileSync(tmp, json, { mode: 0o600 });
|
|
63
|
+
renameSync(tmp, path);
|
|
64
|
+
}
|
|
44
65
|
function writeStored(patch) {
|
|
45
66
|
const merged = { ...readStored(), ...patch };
|
|
46
67
|
mkdirSync(configDir(), { recursive: true });
|
|
47
|
-
|
|
68
|
+
writeAtomic(JSON.stringify(merged, null, 2));
|
|
69
|
+
}
|
|
70
|
+
/** Lockfile que serializa o refresh de token entre processos da CLI (ver auth.ts). */
|
|
71
|
+
export function refreshLockPath() {
|
|
72
|
+
return join(configDir(), 'refresh.lock');
|
|
73
|
+
}
|
|
74
|
+
export function ensureConfigDir() {
|
|
75
|
+
mkdirSync(configDir(), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
/** Remove o lockfile sem erro quando ele já foi liberado por outro processo. */
|
|
78
|
+
export function releaseRefreshLock() {
|
|
79
|
+
try {
|
|
80
|
+
rmSync(refreshLockPath());
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// já removido por outro processo
|
|
84
|
+
}
|
|
48
85
|
}
|
|
49
86
|
export function saveSession(endpoints, tokens) {
|
|
50
87
|
writeStored({ ...endpoints, ...tokens });
|
|
@@ -60,7 +97,7 @@ export function clearSession() {
|
|
|
60
97
|
void accessTokenExpiresAt;
|
|
61
98
|
void email;
|
|
62
99
|
mkdirSync(configDir(), { recursive: true });
|
|
63
|
-
|
|
100
|
+
writeAtomic(JSON.stringify(rest, null, 2));
|
|
64
101
|
return existed;
|
|
65
102
|
}
|
|
66
103
|
export function hasSession() {
|
package/dist/main.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ApiError } from './api.js';
|
|
3
|
-
import { setColorEnabled } from './output.js';
|
|
3
|
+
import { setColorEnabled, yellow } from './output.js';
|
|
4
4
|
import { authCommand } from './commands/auth.js';
|
|
5
5
|
import { inboxCommand } from './commands/inbox.js';
|
|
6
6
|
import { stageCommand } from './commands/stage.js';
|
|
7
|
+
import { updateCommand } from './commands/update.js';
|
|
7
8
|
import { fail, parseArgs as parse } from './commands/shared.js';
|
|
8
|
-
|
|
9
|
+
import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
|
|
10
|
+
const VERSION = '0.3.0';
|
|
9
11
|
const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
|
|
10
12
|
|
|
11
13
|
Uso: memoring <comando> [subcomando] [flags]
|
|
@@ -18,16 +20,21 @@ Comandos:
|
|
|
18
20
|
inbox list itens pendentes de revisão
|
|
19
21
|
inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
|
|
20
22
|
inbox discard <id> descarta um item pendente
|
|
23
|
+
update atualiza a CLI para a versão mais recente (via npm)
|
|
21
24
|
version versão da CLI
|
|
22
25
|
|
|
23
26
|
Flags globais:
|
|
24
27
|
--json saída legível por máquina
|
|
25
28
|
--no-color desativa cores
|
|
26
29
|
--api-base sobrescreve a URL da API (default: https://memoring.com.br/api)
|
|
30
|
+
--version imprime a versão da CLI
|
|
27
31
|
--help, -h ajuda
|
|
28
32
|
|
|
29
33
|
Ambiente (dev): MEMORING_API_BASE, MEMORING_KEYCLOAK_URL, MEMORING_KC_REALM, MEMORING_KC_CLIENT
|
|
30
34
|
|
|
35
|
+
Atualização: a CLI verifica em background (1x/24h) se há versão nova e avisa no stderr.
|
|
36
|
+
MEMORING_NO_UPDATE_CHECK=1 desativa a verificação.
|
|
37
|
+
|
|
31
38
|
Exemplos:
|
|
32
39
|
memoring auth login
|
|
33
40
|
memoring stage schema
|
|
@@ -45,8 +52,21 @@ async function main() {
|
|
|
45
52
|
if (apiBase) {
|
|
46
53
|
process.env.MEMORING_API_BASE = apiBase;
|
|
47
54
|
}
|
|
55
|
+
if (parsed._[0] !== 'update') {
|
|
56
|
+
scheduleUpdateCheck();
|
|
57
|
+
}
|
|
58
|
+
process.on('exit', () => {
|
|
59
|
+
const latest = newerVersionAvailable(VERSION);
|
|
60
|
+
if (latest) {
|
|
61
|
+
process.stderr.write(yellow(`\nnova versão do memoring disponível: ${VERSION} → ${latest} — rode \`memoring update\` para atualizar.\n`));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
48
64
|
const [command, ...rest] = parsed._;
|
|
49
65
|
const sub = { _: rest, flags: parsed.flags };
|
|
66
|
+
if (parsed.flags.version) {
|
|
67
|
+
process.stdout.write(`memoring-cli ${VERSION}\n`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
50
70
|
if (parsed.flags.help || !command) {
|
|
51
71
|
process.stdout.write(HELP);
|
|
52
72
|
return;
|
|
@@ -61,6 +81,9 @@ async function main() {
|
|
|
61
81
|
case 'inbox':
|
|
62
82
|
await inboxCommand(sub);
|
|
63
83
|
return;
|
|
84
|
+
case 'update':
|
|
85
|
+
await updateCommand(VERSION);
|
|
86
|
+
return;
|
|
64
87
|
case 'version':
|
|
65
88
|
process.stdout.write(`memoring-cli ${VERSION}\n`);
|
|
66
89
|
return;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { configDir } from './config.js';
|
|
5
|
+
const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@memoring%2Fcli/latest';
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
8
|
+
function cachePath() {
|
|
9
|
+
return join(configDir(), 'update-check.json');
|
|
10
|
+
}
|
|
11
|
+
function readCache() {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(cachePath(), 'utf8'));
|
|
14
|
+
if (typeof parsed.checkedAt !== 'number') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
checkedAt: parsed.checkedAt,
|
|
19
|
+
latest: typeof parsed.latest === 'string' ? parsed.latest : undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeCache(latest) {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(dirname(cachePath()), { recursive: true });
|
|
29
|
+
const tmp = `${cachePath()}.${process.pid}.tmp`;
|
|
30
|
+
writeFileSync(tmp, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
31
|
+
renameSync(tmp, cachePath());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// cache é best-effort — nunca atrapalhar o comando em execução
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** true quando `latest` é semver maior que `current` (comparação numérica de x.y.z). */
|
|
38
|
+
export function isNewerVersion(latest, current) {
|
|
39
|
+
const parts = (v) => v.replace(/^v/, '').split('.').map((p) => Number.parseInt(p, 10) || 0);
|
|
40
|
+
const a = parts(latest);
|
|
41
|
+
const b = parts(current);
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
const x = a[i] ?? 0;
|
|
44
|
+
const y = b[i] ?? 0;
|
|
45
|
+
if (x !== y) {
|
|
46
|
+
return x > y;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** Versão mais nova segundo o cache local, ou null (cache ausente/stale não é erro). */
|
|
52
|
+
export function newerVersionAvailable(current) {
|
|
53
|
+
const cache = readCache();
|
|
54
|
+
return cache?.latest && isNewerVersion(cache.latest, current) ? cache.latest : null;
|
|
55
|
+
}
|
|
56
|
+
/** Consulta o registry na hora (usado pelo `memoring update`). Null quando offline/indisponível. */
|
|
57
|
+
export async function fetchLatestVersion() {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
headers: { accept: 'application/json' },
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const body = (await response.json());
|
|
69
|
+
return typeof body.version === 'string' ? body.version : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Grava o cache (após self-update bem-sucedido, suprime o aviso na hora). */
|
|
79
|
+
export function recordUpdateCheck(latest) {
|
|
80
|
+
writeCache(latest);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Agenda a verificação periódica: um processo filho detached faz o fetch e grava
|
|
84
|
+
* o cache — o comando em execução nunca espera a rede e nunca vê erro (offline ok).
|
|
85
|
+
* No máximo uma verificação a cada 24h; MEMORING_NO_UPDATE_CHECK=1 desativa.
|
|
86
|
+
*/
|
|
87
|
+
export function scheduleUpdateCheck() {
|
|
88
|
+
if (process.env.MEMORING_NO_UPDATE_CHECK) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const cache = readCache();
|
|
92
|
+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const script = `
|
|
96
|
+
const [cachePath, previous] = process.argv.slice(1);
|
|
97
|
+
const fs = require('node:fs');
|
|
98
|
+
const path = require('node:path');
|
|
99
|
+
const writeCache = (latest) => {
|
|
100
|
+
try {
|
|
101
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
102
|
+
const tmp = cachePath + '.' + process.pid + '.tmp';
|
|
103
|
+
fs.writeFileSync(tmp, JSON.stringify({ checkedAt: Date.now(), latest: latest || undefined }));
|
|
104
|
+
fs.renameSync(tmp, cachePath);
|
|
105
|
+
} catch {}
|
|
106
|
+
};
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), ${FETCH_TIMEOUT_MS});
|
|
109
|
+
fetch(${JSON.stringify(REGISTRY_LATEST_URL)}, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
110
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
111
|
+
.then((b) => writeCache(b && typeof b.version === 'string' ? b.version : previous))
|
|
112
|
+
.catch(() => writeCache(previous))
|
|
113
|
+
.finally(() => clearTimeout(timer));
|
|
114
|
+
`;
|
|
115
|
+
try {
|
|
116
|
+
const child = spawn(process.execPath, ['-e', script, cachePath(), cache?.latest ?? ''], {
|
|
117
|
+
detached: true,
|
|
118
|
+
stdio: 'ignore',
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
child.unref();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// verificação é best-effort
|
|
125
|
+
}
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
|
|
|
33
33
|
## Armadilhas
|
|
34
34
|
|
|
35
35
|
- **Nunca** use `--replace` sem o usuário pedir explicitamente — ele apaga os itens pendentes existentes da caixa.
|
|
36
|
+
- Se aparecer um aviso de versão nova no stderr, ignore e siga o fluxo — **não** rode `memoring update` por conta própria; atualizar a CLI é decisão do usuário.
|
|
36
37
|
- Se `stage create` falhar com erro de validação do servidor, corrija os itens indicados e reenvie — não relaxe o contrato.
|
|
37
38
|
- Lotes grandes: divida em vários arquivos de até 50 itens e envie sequencialmente.
|
|
38
39
|
- Flashcards bons são atômicos: uma ideia por cartão, pergunta direta, resposta completa mas curta.
|