@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 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
- writeFileSync(configPath(), JSON.stringify(merged, null, 2), { mode: 0o600 });
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
- writeFileSync(configPath(), JSON.stringify(rest, null, 2), { mode: 0o600 });
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
- const VERSION = '0.2.0';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI do Memoring — envie flashcards/notas gerados por IA para a sua caixa de revisão, direto do terminal.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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.