moodline 0.1.0 → 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/README.md CHANGED
@@ -16,12 +16,14 @@ A cor da barra é interpolada de forma contínua no espaço HSL, do verde (matiz
16
16
 
17
17
  ## Instalação
18
18
 
19
- Uma linha. Detecta as CLIs instaladas e configura cada uma:
19
+ Uma linha. Abre um **wizard interativo** (logo animado, seletor de CLIs e features):
20
20
 
21
21
  ```bash
22
22
  npx moodline init
23
23
  ```
24
24
 
25
+ A instalação é sempre **global (user-level)** — vale pra todos os projetos daquela CLI, nunca por repositório. Modo não-interativo (CI/scripts): `npx moodline init --all --yes`.
26
+
25
27
  Depois é só abrir uma sessão do Claude Code (ou do Copilot CLI). Pra testar a barra na hora, sem abrir sessão:
26
28
 
27
29
  ```bash
@@ -46,9 +48,9 @@ Resumo: **Claude Code e Copilot CLI funcionam de verdade hoje**, porque comparti
46
48
 
47
49
  ```
48
50
  npx moodline init
49
- └─ copia o engine (lib/moodline-core.mjs) pra ~/.claude/moodline/ e ~/.copilot/moodline/
51
+ └─ copia o engine (moodline-core.mjs + puns.mjs) pra ~/.claude/moodline/ e ~/.copilot/moodline/
50
52
  └─ escreve um config.json com as features escolhidas
51
- └─ aponta o settings.json da CLI pra: node ".../moodline-core.mjs" --adapter=claude --config=...
53
+ └─ aponta o settings.json (user-level) da CLI pra: node ".../moodline-core.mjs" --adapter=claude --config=...
52
54
  ```
53
55
 
54
56
  A cada atualização, a CLI executa o engine passando o JSON da sessão no stdin. O engine **normaliza** os campos (via um *adapter* por CLI), monta a barra e imprime no stdout. Zero dependências de runtime, então o render é rápido.
@@ -76,14 +78,28 @@ npx moodline init --all # força Claude Code E Copilot CLI
76
78
  ## Comandos
77
79
 
78
80
  ```
79
- moodline init Configura a statusline nas CLIs detectadas
81
+ moodline init Wizard de instalação (interativo) escopo global
82
+ moodline enable Liga a statusline [--all | --claude | --copilot]
83
+ moodline disable Desliga (mantém config; re-enable instantâneo)
84
+ moodline doctor Mostra o que está instalado e ligado
85
+ moodline uninstall Remove a statusLine [--purge apaga o engine]
80
86
  moodline render Lê JSON no stdin e imprime a barra (teste)
81
- moodline doctor Mostra o que está instalado e configurado
82
- moodline uninstall Remove a statusLine das CLIs
83
87
  moodline watch [experimental] Poller pro OpenCode → stdout
84
88
  moodline --help
85
89
  ```
86
90
 
91
+ ### Ligar e desligar
92
+
93
+ Habilite ou desabilite por CLI, sem perder a configuração — ideal pra alternar dentro do Claude Code ou do Copilot:
94
+
95
+ ```bash
96
+ moodline disable --claude # desliga só no Claude Code
97
+ moodline enable --copilot # liga só no Copilot CLI
98
+ moodline disable --all # desliga em todas
99
+ ```
100
+
101
+ `disable` só remove a chave `statusLine` do `settings.json`; o engine e o `config.json` ficam, então `enable` volta na hora.
102
+
87
103
  ## Configuração manual
88
104
 
89
105
  O `init` faz tudo, mas se preferir na mão — `~/.claude/settings.json`:
@@ -132,7 +148,16 @@ npm test # smoke tests (sem framework, só node)
132
148
  echo '{"model":{"display_name":"Opus"},"context_window":{"used_percentage":50,"total_input_tokens":100000}}' | node bin/moodline.js render
133
149
  ```
134
150
 
135
- Arquitetura: `lib/moodline-core.mjs` é o engine auto-contido (render + adapters + git + puns). `bin/moodline.js` é o instalador/CLI. Adicionar uma CLI = escrever um adapter `fromX(json)` que normaliza pro mesmo formato de estado.
151
+ Arquitetura (arquivos separados de propósito):
152
+
153
+ - `lib/moodline-core.mjs` — engine (render + adapters + git). Importa só `./puns.mjs` e built-ins.
154
+ - `lib/puns.mjs` — lista de trocadilhos PT-BR (o arquivo mais fácil de editar/crescer).
155
+ - `lib/logo.mjs` — logo ASCII + render com gradiente + animação de onda.
156
+ - `lib/ui.mjs` — prompts interativos (multiselect/select/confirm) e spinner, em `node:readline` puro.
157
+ - `lib/install.mjs` — instalar/enable/disable/uninstall (sempre user-level; aceita `home` pra testes).
158
+ - `bin/moodline.js` — dispatcher fino dos comandos.
159
+
160
+ O `init` copia só os arquivos do engine (`moodline-core.mjs` + `puns.mjs`) pra dentro da CLI. Adicionar uma CLI = escrever um adapter `fromX(json)` em `moodline-core.mjs` que normaliza pro mesmo formato de estado.
136
161
 
137
162
  ### Release
138
163
 
package/bin/moodline.js CHANGED
@@ -1,244 +1,200 @@
1
1
  #!/usr/bin/env node
2
- // moodline — instalador/configurador da statusline para CLIs de IA.
2
+ // moodline — instalador/CLI. Escopo de instalacao SEMPRE global (user-level).
3
3
  //
4
- // Comandos:
5
- // moodline init configura a(s) CLI(s) detectada(s) (Claude Code, Copilot CLI)
6
- // moodline render le JSON no stdin e imprime a barra (pra testar)
7
- // moodline doctor mostra o que esta instalado/configurado
8
- // moodline uninstall remove a statusLine das CLIs
9
- // moodline watch [EXPERIMENTAL] poller pro OpenCode (HTTP) -> stdout
4
+ // moodline init wizard interativo (logo animado + seletor de CLIs/features)
5
+ // moodline enable [--all] liga a statusline (Claude Code / Copilot CLI)
6
+ // moodline disable [--all] desliga (mantem config; re-enable instantaneo)
7
+ // moodline doctor mostra estado
8
+ // moodline uninstall [--purge] remove a statusLine (e o engine com --purge)
9
+ // moodline render le JSON no stdin e imprime a barra (teste)
10
+ // moodline watch [experimental] poller pro OpenCode
10
11
  // moodline --help|--version
11
12
 
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
13
- import { homedir } from 'node:os';
13
+ import { readFileSync } from 'node:fs';
14
14
  import { join, dirname } from 'node:path';
15
- import { fileURLToPath, pathToFileURL } from 'node:url';
16
- import { render, ADAPTERS, fromOpenCode, loadConfig, attachGit, DEFAULT_CFG } from '../lib/moodline-core.mjs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { render, ADAPTERS, attachGit, loadConfig, fromOpenCode } from '../lib/moodline-core.mjs';
17
+ import * as ui from '../lib/ui.mjs';
18
+ import { printLogo, smallLogo } from '../lib/logo.mjs';
19
+ import * as install from '../lib/install.mjs';
17
20
 
21
+ const C = ui.C;
18
22
  const HERE = dirname(fileURLToPath(import.meta.url));
19
- const CORE_SRC = join(HERE, '..', 'lib', 'moodline-core.mjs');
20
23
  const PKG = JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8'));
21
24
 
22
- const C = { cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', red: '\x1b[31m', reset: '\x1b[0m', bold: '\x1b[1m' };
23
- const ok = (s) => console.log(`${C.green}✓${C.reset} ${s}`);
24
- const warn = (s) => console.log(`${C.yellow}!${C.reset} ${s}`);
25
- const info = (s) => console.log(` ${C.dim}${s}${C.reset}`);
26
-
27
- // --------- arg parsing ---------
28
25
  function parseArgs(argv) {
29
26
  const o = { _: [] };
30
27
  for (const a of argv) {
31
- if (a.startsWith('--')) {
32
- const [k, v] = a.slice(2).split('=');
33
- o[k] = v === undefined ? true : v;
34
- } else o._.push(a);
28
+ if (a.startsWith('--')) { const [k, v] = a.slice(2).split('='); o[k] = v === undefined ? true : v; }
29
+ else o._.push(a);
35
30
  }
36
31
  return o;
37
32
  }
38
33
 
39
- // --------- alvos de CLI suportados (statusLine nativa por comando + JSON no stdin) ---------
40
- const TARGETS = {
41
- claude: {
42
- label: 'Claude Code',
43
- dir: join(homedir(), '.claude'),
44
- settings: join(homedir(), '.claude', 'settings.json'),
45
- adapter: 'claude',
46
- patch(s, command) {
47
- s.statusLine = { type: 'command', command, padding: 0, refreshInterval: 5 };
48
- },
49
- },
50
- copilot: {
51
- label: 'GitHub Copilot CLI',
52
- dir: join(homedir(), '.copilot'),
53
- settings: join(homedir(), '.copilot', 'settings.json'),
54
- adapter: 'copilot',
55
- patch(s, command) {
56
- s.statusLine = { type: 'command', command, padding: 1 };
57
- // statusLine no Copilot e experimental: liga a feature flag
58
- s.feature_flags = s.feature_flags || {};
59
- s.feature_flags.enabled = Array.from(new Set([...(s.feature_flags.enabled || []), 'STATUS_LINE']));
60
- },
61
- },
62
- };
63
-
64
- function readJson(path) {
65
- if (!existsSync(path)) return {};
66
- try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
67
- }
68
- function fwd(p) { return p.replace(/\\/g, '/'); }
69
-
70
- function buildFeatures(opts) {
71
- const f = structuredClone(DEFAULT_CFG.features);
72
- if (typeof opts.features === 'string') {
73
- const want = new Set(opts.features.split(',').map((x) => x.trim()).filter(Boolean));
74
- for (const k of Object.keys(f)) f[k] = want.has(k);
34
+ const ALL_FEATURES = ['git', 'cost', 'rate', 'puns'];
35
+
36
+ async function cmdInit(opts) {
37
+ await printLogo({ animate: ui.isInteractive() && !opts['no-anim'] });
38
+ const home = opts.home;
39
+ const detected = install.detectInstalled(home);
40
+ const interactive = ui.isInteractive() && !opts.yes && !opts['no-input'];
41
+
42
+ let keys, features, layout;
43
+ if (interactive) {
44
+ const cliChoices = detected.map((d) => ({
45
+ name: d.present ? d.label : `${d.label} ${C.dim}(não detectado)${C.reset}`,
46
+ value: d.key, checked: d.present,
47
+ }));
48
+ keys = await ui.multiselect('Em quais CLIs instalar a statusline?', cliChoices);
49
+ if (!keys.length) { console.log(`${C.yellow}Nada selecionado. Saindo.${C.reset}`); return; }
50
+
51
+ features = await ui.multiselect('Quais extras ligar?', [
52
+ { name: 'Git branch + estado (dirty/ahead/behind)', value: 'git', checked: true },
53
+ { name: 'Custo USD + tempo + linhas +/-', value: 'cost', checked: true },
54
+ { name: 'Rate limits 5h/7d (Claude Pro/Max)', value: 'rate', checked: true },
55
+ { name: 'Trocadilhos de dev 💬', value: 'puns', checked: true },
56
+ ]);
57
+ layout = await ui.select('Layout da barra?', [
58
+ { name: 'Uma linha (compacto)', value: 'single' },
59
+ { name: 'Duas linhas (mais informação)', value: 'multi' },
60
+ ]);
61
+ } else {
62
+ keys = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
63
+ if (!keys.length) keys = detected.filter((d) => d.present || d.key === 'claude').map((d) => d.key);
64
+ features = typeof opts.features === 'string'
65
+ ? opts.features.split(',').map((s) => s.trim()).filter(Boolean)
66
+ : ALL_FEATURES.filter((k) => !opts[`no-${k}`]);
67
+ layout = opts.multi ? 'multi' : 'single';
75
68
  }
76
- for (const k of Object.keys(f)) {
77
- if (opts[`no-${k}`]) f[k] = false;
69
+
70
+ console.log();
71
+ for (const key of keys) {
72
+ const t = install.targets(home)[key];
73
+ const sp = ui.spinner(`Configurando ${t.label}…`);
74
+ try { install.configure(key, { features, layout, home }); sp.stop(true, `${t.label} ${C.green}pronto${C.reset}`); }
75
+ catch (e) { sp.stop(false, `${t.label}: ${e.message}`); }
78
76
  }
79
- return f;
77
+ postInstall(keys);
80
78
  }
81
79
 
82
- function configureTarget(key, opts) {
83
- const t = TARGETS[key];
84
- const root = join(t.dir, 'moodline');
85
- const coreDest = join(root, 'moodline-core.mjs');
86
- const cfgDest = join(root, 'config.json');
87
-
88
- mkdirSync(root, { recursive: true });
89
- copyFileSync(CORE_SRC, coreDest);
90
-
91
- const cfg = { ...structuredClone(DEFAULT_CFG), features: buildFeatures(opts) };
92
- if (opts.multi) cfg.layout = 'multi';
93
- writeFileSync(cfgDest, JSON.stringify(cfg, null, 2) + '\n');
94
-
95
- const command = `node "${fwd(coreDest)}" --adapter=${t.adapter} --config="${fwd(cfgDest)}"`;
96
- const settings = readJson(t.settings);
97
- t.patch(settings, command);
98
- mkdirSync(dirname(t.settings), { recursive: true });
99
- writeFileSync(t.settings, JSON.stringify(settings, null, 2) + '\n');
100
-
101
- ok(`${t.label} configurado`);
102
- info(`engine: ${fwd(coreDest)}`);
103
- info(`config: ${fwd(cfgDest)}`);
104
- info(`settings: ${fwd(t.settings)}`);
105
- return { coreDest, cfgDest, command };
80
+ function postInstall(keys) {
81
+ console.log(`\n${C.green}✓ Instalado${C.reset} (global, user-level). Abra uma sessão da CLI pra ver a barra.`);
82
+ if (keys.includes('copilot')) {
83
+ console.log(`${C.yellow}!${C.reset} Copilot CLI: statusLine é experimental — se não aparecer, rode ${C.cyan}/experimental${C.reset} ou reinicie.`);
84
+ }
85
+ console.log(`\n${C.dim}Ligar/desligar quando quiser:${C.reset}`);
86
+ console.log(` ${C.cyan}moodline disable${C.reset} ${C.dim}# desliga (mantém config)${C.reset}`);
87
+ console.log(` ${C.cyan}moodline enable${C.reset} ${C.dim}# liga de novo${C.reset}`);
88
+ console.log(`\n${C.dim}Teste agora:${C.reset} ${C.cyan}moodline render${C.reset}`);
106
89
  }
107
90
 
108
- function cmdInit(opts) {
109
- console.log(`${C.bold}\u{1F33F} moodline init${C.reset}\n`);
110
-
111
- const explicit = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
112
- let keys;
113
- if (explicit.length) {
114
- keys = explicit;
115
- } else {
116
- // default: Claude Code sempre; Copilot se detectado
117
- keys = ['claude'];
118
- if (existsSync(TARGETS.copilot.dir)) keys.push('copilot');
91
+ function resolveKeys(opts, needConfigured) {
92
+ let keys = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
93
+ if (!keys.length) {
94
+ const det = install.detectInstalled(opts.home);
95
+ keys = det.filter((d) => (needConfigured ? d.engine || d.wired : d.present)).map((d) => d.key);
119
96
  }
97
+ return keys;
98
+ }
120
99
 
121
- for (const k of keys) configureTarget(k, opts);
122
-
123
- console.log();
124
- if (keys.includes('copilot')) {
125
- warn('Copilot CLI: statusLine e experimental — reinicie o copilot ou rode `/experimental` se a barra nao aparecer.');
100
+ function cmdToggle(opts, enabled) {
101
+ const keys = resolveKeys(opts, true);
102
+ if (!keys.length) { console.log(`${C.yellow}Nada configurado.${C.reset} Rode ${C.cyan}moodline init${C.reset}.`); return; }
103
+ for (const key of keys) {
104
+ const t = install.targets(opts.home)[key];
105
+ try { install.setEnabled(key, enabled, { home: opts.home }); console.log(`${C.green}✓${C.reset} ${t.label}: statusline ${enabled ? 'habilitada' : 'desabilitada'}`); }
106
+ catch (e) { console.log(`${C.yellow}!${C.reset} ${e.message}`); }
126
107
  }
127
- console.log(`\n${C.bold}Outras CLIs:${C.reset}`);
128
- info('Gemini CLI — sem statusLine por comando; so footer fixo (toggles) ou extensao HUD 3rd-party. [experimental]');
129
- info('OpenCode — sem statusLine na TUI; use `moodline watch` num painel tmux/zellij. [experimental]');
130
- info('Junie CLI — nao suporta statusline (hook SessionStart descarta o output). Sem suporte.');
131
- console.log(`\n${C.green}Pronto.${C.reset} Abra uma sessao da CLI pra ver a barra. Teste agora: ${C.cyan}moodline render${C.reset}`);
132
108
  }
133
109
 
134
110
  function cmdUninstall(opts) {
135
- const keys = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
136
- const list = keys.length ? keys : ['claude', 'copilot'];
137
- for (const k of list) {
138
- const t = TARGETS[k];
139
- if (!existsSync(t.settings)) continue;
140
- const s = readJson(t.settings);
141
- if (s.statusLine) { delete s.statusLine; writeFileSync(t.settings, JSON.stringify(s, null, 2) + '\n'); ok(`${t.label}: statusLine removida`); }
142
- else info(`${t.label}: nada pra remover`);
111
+ const keys = resolveKeys(opts, true);
112
+ if (!keys.length) { console.log('Nada pra remover.'); return; }
113
+ for (const key of keys) {
114
+ const t = install.targets(opts.home)[key];
115
+ const r = install.uninstall(key, { home: opts.home, purge: opts.purge });
116
+ console.log(`${C.green}✓${C.reset} ${t.label}: ${r.had ? 'statusLine removida' : 'nada na statusLine'}${opts.purge ? ' + engine apagado' : ''}`);
143
117
  }
144
118
  }
145
119
 
146
- function cmdDoctor() {
147
- console.log(`${C.bold}moodline doctor${C.reset} v${PKG.version}\n`);
148
- info(`home: ${fwd(homedir())}`);
149
- for (const [k, t] of Object.entries(TARGETS)) {
150
- const has = existsSync(t.dir);
151
- const s = readJson(t.settings);
152
- const wired = !!s.statusLine && /moodline-core\.mjs/.test(s.statusLine.command || '');
153
- console.log(`\n${C.bold}${t.label}${C.reset} (${k})`);
154
- info(`dir existe: ${has ? 'sim' : 'nao'}`);
155
- info(`statusLine moodline: ${wired ? C.green + 'ativa' + C.reset : 'nao'}`);
156
- if (s.statusLine?.command) info(`command: ${s.statusLine.command}`);
120
+ function cmdDoctor(opts) {
121
+ console.log(`${smallLogo()} ${C.dim}v${PKG.version}${C.reset}\n`);
122
+ for (const d of install.detectInstalled(opts.home)) {
123
+ const state = d.wired ? `${C.green}ativa${C.reset}` : d.engine ? `${C.yellow}configurada, desligada${C.reset}` : `${C.dim}não instalada${C.reset}`;
124
+ console.log(`${C.bold}${d.label}${C.reset}`);
125
+ console.log(` dir detectado: ${d.present ? 'sim' : 'não'} statusline: ${state}`);
157
126
  }
158
127
  }
159
128
 
160
129
  function cmdRender(opts) {
161
- // delega pro engine: le stdin, imprime barra. Reusa as mesmas flags.
162
- const adapterName = (opts.adapter || 'claude').toLowerCase();
163
- const adapter = ADAPTERS[adapterName] || ADAPTERS.claude;
130
+ const adapter = ADAPTERS[(opts.adapter || 'claude').toLowerCase()] || ADAPTERS.claude;
164
131
  const cfg = loadConfig(opts.config);
165
- for (const k of ['git', 'cost', 'rate', 'puns']) if (opts[`no-${k}`]) cfg.features[k] = false;
132
+ for (const k of ALL_FEATURES) if (opts[`no-${k}`]) cfg.features[k] = false;
166
133
  if (opts.multi) cfg.layout = 'multi';
167
134
  if (opts.width) cfg.width = parseInt(opts.width, 10) || undefined;
168
135
 
169
136
  let raw = '';
170
137
  try { raw = readFileSync(0, 'utf8'); } catch {}
171
138
  if (!raw.trim()) {
172
- // sem stdin: demo
139
+ console.log(`${C.dim}sem stdin demo:${C.reset}`);
173
140
  raw = JSON.stringify({ model: { display_name: 'Opus' }, effort: { level: 'high' }, context_window: { used_percentage: 42, total_input_tokens: 84000 }, cost: { total_cost_usd: 0.12, total_duration_ms: 320000, total_lines_added: 120, total_lines_removed: 30 } });
174
- warn('sem stdin — mostrando demo:');
175
141
  }
176
- let j = {};
177
- try { j = JSON.parse(raw); } catch {}
178
- const state = attachGit(adapter(j), cfg.features.git);
179
- console.log(render(state, cfg));
142
+ let j = {}; try { j = JSON.parse(raw); } catch {}
143
+ console.log(render(attachGit(adapter(j), cfg.features.git), cfg));
180
144
  }
181
145
 
182
146
  async function cmdWatch(opts) {
183
- // EXPERIMENTAL: poller pro OpenCode. Endpoint pode variar por versao.
184
- const port = opts.port || 4096;
185
- const url = opts.url || `http://127.0.0.1:${port}/session`;
147
+ const url = opts.url || `http://127.0.0.1:${opts.port || 4096}/session`;
186
148
  const cfg = loadConfig(opts.config);
187
- warn(`[experimental] OpenCode watch: GET ${url} a cada ${opts.interval || 2}s`);
188
149
  const interval = (parseInt(opts.interval, 10) || 2) * 1000;
150
+ console.log(`${C.yellow}[experimental]${C.reset} OpenCode watch: GET ${url}`);
189
151
  const tick = async () => {
190
152
  try {
191
153
  const r = await fetch(url);
192
154
  const j = await r.json();
193
- const state = fromOpenCode(Array.isArray(j) ? j[0] : j);
194
- process.stdout.write('\r\x1b[K' + render(state, cfg));
195
- } catch (e) {
196
- process.stdout.write('\r\x1b[K' + C.dim + 'moodline: aguardando OpenCode em ' + url + C.reset);
197
- }
155
+ process.stdout.write('\r\x1b[K' + render(fromOpenCode(Array.isArray(j) ? j[0] : j), cfg));
156
+ } catch { process.stdout.write(`\r\x1b[K${C.dim}aguardando OpenCode em ${url}${C.reset}`); }
198
157
  };
199
158
  await tick();
200
159
  setInterval(tick, interval);
201
160
  }
202
161
 
203
162
  function cmdHelp() {
204
- console.log(`${C.bold}\u{1F33F} moodline${C.reset} v${PKG.version} — statusline divertida pra CLIs de IA
163
+ console.log(`${smallLogo()} ${C.dim}v${PKG.version}${C.reset} — statusline divertida pra CLIs de IA
205
164
 
206
- ${C.bold}Uso:${C.reset}
207
- npx moodline <comando> [opcoes]
165
+ ${C.bold}Uso:${C.reset} npx moodline <comando> [opções]
208
166
 
209
167
  ${C.bold}Comandos:${C.reset}
210
- init Configura a statusline nas CLIs detectadas (Claude Code, Copilot CLI)
211
- render Le JSON no stdin e imprime a barra (pra testar)
212
- doctor Mostra o que esta instalado e configurado
213
- uninstall Remove a statusLine das CLIs
214
- watch [experimental] Poller pro OpenCode -> stdout (use num painel tmux)
215
-
216
- ${C.bold}Opcoes do init:${C.reset}
217
- --all Configura Claude Code E Copilot CLI
218
- --claude / --copilot So a CLI escolhida
219
- --features=git,cost Liga so essas features (git,cost,rate,puns)
220
- --no-puns --no-rate Desliga uma feature especifica
221
- --multi Layout em 2 linhas
222
-
223
- ${C.bold}Exemplos:${C.reset}
224
- npx moodline init
225
- npx moodline init --all --no-rate
168
+ ${C.cyan}init${C.reset} Wizard de instalação (interativo) escopo global
169
+ ${C.cyan}enable${C.reset} Liga a statusline ${C.dim}[--all | --claude | --copilot]${C.reset}
170
+ ${C.cyan}disable${C.reset} Desliga (mantém config; re-enable instantâneo)
171
+ ${C.cyan}doctor${C.reset} Mostra o que está instalado e ligado
172
+ ${C.cyan}uninstall${C.reset} Remove a statusLine ${C.dim}[--purge apaga o engine]${C.reset}
173
+ ${C.cyan}render${C.reset} Lê JSON no stdin e imprime a barra (teste)
174
+ ${C.cyan}watch${C.reset} [experimental] Poller pro OpenCode → stdout
175
+
176
+ ${C.bold}init não-interativo:${C.reset}
177
+ --all | --claude | --copilot escolhe a(s) CLI(s)
178
+ --features=git,cost,rate,puns liga essas
179
+ --no-puns --no-rate desliga uma feature
180
+ --multi layout em 2 linhas
181
+ --yes pula o wizard (usa defaults/flags)
182
+
183
+ ${C.bold}Exemplo:${C.reset}
226
184
  echo '{"model":{"display_name":"Opus"},"context_window":{"used_percentage":92,"total_input_tokens":184000}}' | npx moodline render
227
185
  `);
228
186
  }
229
187
 
230
- // --------- dispatch ---------
231
- const argv = process.argv.slice(2);
232
- const opts = parseArgs(argv);
233
- const cmd = opts._[0] || (opts.version ? 'version' : opts.help ? 'help' : 'help');
234
-
188
+ const opts = parseArgs(process.argv.slice(2));
189
+ const cmd = opts._[0] || (opts.version ? 'version' : 'help');
235
190
  switch (cmd) {
236
- case 'init': cmdInit(opts); break;
191
+ case 'init': await cmdInit(opts); break;
192
+ case 'enable': cmdToggle(opts, true); break;
193
+ case 'disable': cmdToggle(opts, false); break;
237
194
  case 'uninstall': cmdUninstall(opts); break;
238
- case 'doctor': cmdDoctor(); break;
195
+ case 'doctor': cmdDoctor(opts); break;
239
196
  case 'render': cmdRender(opts); break;
240
197
  case 'watch': await cmdWatch(opts); break;
241
198
  case 'version': console.log(PKG.version); break;
242
- case 'help':
243
- default: cmdHelp(); break;
199
+ default: cmdHelp();
244
200
  }
@@ -0,0 +1,99 @@
1
+ // install.mjs — logica de instalacao/toggle. SOMENTE escopo global (user-level):
2
+ // ~/.claude/settings.json e ~/.copilot/settings.json. Nunca escreve em .claude de projeto.
3
+ // Todas as funcoes aceitam `home` opcional (default os.homedir()) pra permitir testes em sandbox.
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { DEFAULT_CFG } from './moodline-core.mjs';
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ // Arquivos do engine copiados juntos pro dir da CLI (core importa ./puns.mjs em runtime).
12
+ export const ENGINE_FILES = ['moodline-core.mjs', 'puns.mjs'];
13
+
14
+ export function targets(home = homedir()) {
15
+ const mk = (key, label, sub, adapter) => {
16
+ const dir = join(home, sub);
17
+ const engineDir = join(dir, 'moodline');
18
+ return {
19
+ key, label, adapter, dir, engineDir,
20
+ settings: join(dir, 'settings.json'),
21
+ core: join(engineDir, 'moodline-core.mjs'),
22
+ config: join(engineDir, 'config.json'),
23
+ };
24
+ };
25
+ return {
26
+ claude: mk('claude', 'Claude Code', '.claude', 'claude'),
27
+ copilot: mk('copilot', 'GitHub Copilot CLI', '.copilot', 'copilot'),
28
+ };
29
+ }
30
+
31
+ const fwd = (p) => p.replace(/\\/g, '/');
32
+ function readJson(p) { if (!existsSync(p)) return {}; try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return {}; } }
33
+ function writeJson(p, o) { mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, JSON.stringify(o, null, 2) + '\n'); }
34
+
35
+ function buildFeatures(list) {
36
+ const f = structuredClone(DEFAULT_CFG.features);
37
+ if (Array.isArray(list)) for (const k of Object.keys(f)) f[k] = list.includes(k);
38
+ return f;
39
+ }
40
+ function commandFor(t) {
41
+ return `node "${fwd(t.core)}" --adapter=${t.adapter} --config="${fwd(t.config)}"`;
42
+ }
43
+ function setStatusLine(s, t) {
44
+ const command = commandFor(t);
45
+ if (t.adapter === 'copilot') {
46
+ s.statusLine = { type: 'command', command, padding: 1 };
47
+ // statusLine no Copilot e experimental: garante a feature flag
48
+ s.feature_flags = s.feature_flags || {};
49
+ s.feature_flags.enabled = Array.from(new Set([...(s.feature_flags.enabled || []), 'STATUS_LINE']));
50
+ } else {
51
+ s.statusLine = { type: 'command', command, padding: 0, refreshInterval: 5 };
52
+ }
53
+ }
54
+
55
+ // Copia o engine, escreve o config e liga a statusLine.
56
+ export function configure(key, { features, layout, home } = {}) {
57
+ const t = targets(home)[key];
58
+ mkdirSync(t.engineDir, { recursive: true });
59
+ for (const f of ENGINE_FILES) copyFileSync(join(HERE, f), join(t.engineDir, f));
60
+ const cfg = { ...structuredClone(DEFAULT_CFG), features: buildFeatures(features) };
61
+ if (layout) cfg.layout = layout;
62
+ writeJson(t.config, cfg);
63
+ const s = readJson(t.settings);
64
+ setStatusLine(s, t);
65
+ writeJson(t.settings, s);
66
+ return t;
67
+ }
68
+
69
+ // Habilita/desabilita sem destruir config — re-enable e instantaneo.
70
+ export function setEnabled(key, enabled, { home } = {}) {
71
+ const t = targets(home)[key];
72
+ const s = readJson(t.settings);
73
+ if (enabled) {
74
+ if (!existsSync(t.core)) throw new Error(`${t.label}: não configurado — rode 'moodline init' primeiro`);
75
+ setStatusLine(s, t);
76
+ } else {
77
+ delete s.statusLine; // mantem engine + config.json
78
+ }
79
+ writeJson(t.settings, s);
80
+ return t;
81
+ }
82
+
83
+ export function uninstall(key, { home, purge } = {}) {
84
+ const t = targets(home)[key];
85
+ const s = readJson(t.settings);
86
+ const had = !!s.statusLine;
87
+ delete s.statusLine;
88
+ writeJson(t.settings, s);
89
+ if (purge && existsSync(t.engineDir)) rmSync(t.engineDir, { recursive: true, force: true });
90
+ return { ...t, had };
91
+ }
92
+
93
+ export function detectInstalled(home) {
94
+ return Object.values(targets(home)).map((t) => {
95
+ const s = readJson(t.settings);
96
+ const wired = !!s.statusLine && /moodline-core\.mjs/.test(s.statusLine?.command || '');
97
+ return { ...t, present: existsSync(t.dir), engine: existsSync(t.core), wired };
98
+ });
99
+ }
package/lib/logo.mjs ADDED
@@ -0,0 +1,82 @@
1
+ // logo.mjs — logo ASCII do moodline + render com gradiente + animacao de onda.
2
+ // Usado so no instalador (nao entra no caminho da barra). Auto-contido.
3
+
4
+ const ESC = '\x1b';
5
+ const RESET = `${ESC}[0m`;
6
+
7
+ // HSL -> RGB (h em graus 0..360, s/l em 0..1)
8
+ function hsl(h, s, l) {
9
+ const c = (1 - Math.abs(2 * l - 1)) * s;
10
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
11
+ const m = l - c / 2;
12
+ let r = 0, g = 0, b = 0;
13
+ if (h < 60) [r, g, b] = [c, x, 0];
14
+ else if (h < 120) [r, g, b] = [x, c, 0];
15
+ else if (h < 180) [r, g, b] = [0, c, x];
16
+ else if (h < 240) [r, g, b] = [0, x, c];
17
+ else if (h < 300) [r, g, b] = [x, 0, c];
18
+ else [r, g, b] = [c, 0, x];
19
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
20
+ }
21
+ const tc = (r, g, b) => `${ESC}[38;2;${r};${g};${b}m`;
22
+
23
+ // Glyphs (fonte ANSI Shadow). Cada letra: 6 linhas de largura igual.
24
+ const G = {
25
+ M: ['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
26
+ O: [' ██████╗ ', '██╔═══██╗', '██║ ██║', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '],
27
+ D: ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '],
28
+ L: ['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
29
+ I: ['██╗ ', '██║ ', '██║ ', '██║ ', '██║ ', '╚═╝ '],
30
+ N: ['███╗ ██╗', '████╗ ██║', '██╔██╗ ██║', '██║╚██╗██║', '██║ ╚████║', '╚═╝ ╚═══╝'],
31
+ E: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
32
+ };
33
+ const WORD = 'MOODLINE';
34
+ export const LOGO_LINES = Array.from({ length: 6 }, (_, r) => [...WORD].map((ch) => G[ch][r]).join(' '));
35
+ const MAXW = Math.max(...LOGO_LINES.map((l) => [...l].length));
36
+ export const TAGLINE = '🌿 statusline divertida e informativa pra CLIs de IA';
37
+
38
+ // Colore cada coluna por matiz, com deslocamento de fase (onda verde->vermelho).
39
+ export function renderLogo(phase = 0) {
40
+ return LOGO_LINES.map((line) => {
41
+ const chars = [...line];
42
+ let out = '';
43
+ for (let x = 0; x < chars.length; x++) {
44
+ const ch = chars[x];
45
+ if (ch === ' ') { out += ' '; continue; }
46
+ const hue = (((x / MAXW) * 120 + phase) % 120 + 120) % 120; // 0..120 (verde->vermelho)
47
+ const [r, g, b] = hsl(hue, 1, 0.55);
48
+ out += tc(r, g, b) + ch;
49
+ }
50
+ return out + RESET;
51
+ }).join('\n');
52
+ }
53
+
54
+ // Versao compacta pra terminais estreitos
55
+ export function smallLogo() {
56
+ const [r, g, b] = hsl(90, 1, 0.55);
57
+ return `${tc(r, g, b)}🌿 moodline${RESET}`;
58
+ }
59
+
60
+ const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
61
+
62
+ // Imprime o logo. Anima (onda) se for TTY e couber; senao imprime estatico.
63
+ export async function printLogo({ animate = true, frames = 16, delay = 45 } = {}) {
64
+ const out = process.stdout;
65
+ const cols = out.columns || parseInt(process.env.COLUMNS || '80', 10);
66
+ if (cols < MAXW) { out.write('\n' + smallLogo() + '\n' + '\x1b[2m' + TAGLINE + '\x1b[0m\n\n'); return; }
67
+
68
+ if (!animate || !out.isTTY) {
69
+ out.write('\n' + renderLogo(0) + '\n\x1b[2m ' + TAGLINE + '\x1b[0m\n\n');
70
+ return;
71
+ }
72
+ out.write('\x1b[?25l'); // esconde cursor
73
+ out.write('\n');
74
+ for (let i = 0; i < frames; i++) {
75
+ if (i > 0) out.write(`\x1b[${LOGO_LINES.length}A`); // sobe N linhas
76
+ const block = renderLogo(i * 9).split('\n').map((l) => '\x1b[0G\x1b[K' + l).join('\n');
77
+ out.write(block + '\n');
78
+ await sleep(delay);
79
+ }
80
+ out.write('\x1b[2m ' + TAGLINE + '\x1b[0m\n\n');
81
+ out.write('\x1b[?25h'); // mostra cursor
82
+ }
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, existsSync } from 'node:fs';
13
13
  import { execFileSync } from 'node:child_process';
14
14
  import { pathToFileURL } from 'node:url';
15
+ import { PUNS } from './puns.mjs';
15
16
 
16
17
  // ---------------- ANSI ----------------
17
18
  const ESC = '\x1b';
@@ -70,30 +71,13 @@ function mood(p) {
70
71
  return '\u{1F60E}'; // de boa
71
72
  }
72
73
 
73
- // ---------------- trocadilhos de dev (PT-BR) ----------------
74
- const PUNS = [
75
- 'git push? mais pra git shove',
76
- 'funciona na minha maquina ¯\\_(ツ)_/¯',
77
- 'bug? feature nao documentada',
78
- 'merge sem conflito e merge mentiroso',
79
- 'CSS: Continua Sem Solucao',
80
- '404: piada nao encontrada',
81
- 'deploy na sexta, coragem de sobra',
82
- 'cache: o problema e a solucao',
83
- 'recursao: ver "recursao"',
84
- 'prod de pe? reza o commit',
85
- 'TODO: escrever um TODO melhor',
86
- 'semicolon ; o heroi anonimo',
87
- 'rebase com fe, push com medo',
88
- 'git blame aponta pra mim de novo',
89
- 'tabs vs spaces: a guerra eterna',
90
- 'commit -m "ajustes"',
91
- 'tava funcionando ontem',
92
- 'dark mode pra esconder os bugs',
93
- ];
94
- function pickPun(rotateMs) {
95
- const idx = Math.floor(Date.now() / Math.max(1000, rotateMs)) % PUNS.length;
96
- return PUNS[idx];
74
+ // ---------------- trocadilhos de dev (lista em ./puns.mjs) ----------------
75
+ // Rotaciona devagar (por janela de tempo) pra mudar sem piscar. `extra` permite
76
+ // adicionar trocadilhos via config.json sem editar o engine.
77
+ function pickPun(rotateMs, extra = []) {
78
+ const pool = Array.isArray(extra) && extra.length ? PUNS.concat(extra) : PUNS;
79
+ const idx = Math.floor(Date.now() / Math.max(1000, rotateMs)) % pool.length;
80
+ return pool[idx];
97
81
  }
98
82
 
99
83
  // ---------------- formatadores ----------------
@@ -196,7 +180,7 @@ export function render(state, cfg = DEFAULT_CFG) {
196
180
  }
197
181
 
198
182
  if (f.puns) {
199
- opt.push({ txt: `${DIM}\u{1F4AC} ${pickPun(cfg.punRotateMs ?? 30000)}${RESET}`, prio: 4 });
183
+ opt.push({ txt: `${DIM}\u{1F4AC} ${pickPun(cfg.punRotateMs ?? 30000, cfg.extraPuns)}${RESET}`, prio: 4 });
200
184
  }
201
185
 
202
186
  const sep = ` ${DIM}·${RESET} `;
package/lib/puns.mjs ADDED
@@ -0,0 +1,50 @@
1
+ // puns.mjs — trocadilhos/piadas de dev (PT-BR). Arquivo separado de proposito: e o
2
+ // mais fácil de editar e o que mais cresce. Copiado junto do engine no `moodline init`.
3
+ // Regras: curto (a barra trunca), PT-BR, sem quebrar em terminal estreito.
4
+ export const PUNS = [
5
+ 'git push? mais pra git shove',
6
+ 'funciona na minha máquina ¯\\_(ツ)_/¯',
7
+ 'bug? feature não documentada',
8
+ 'merge sem conflito é merge mentiroso',
9
+ 'CSS: Continua Sem Solução',
10
+ '404: piada não encontrada',
11
+ 'deploy na sexta, coragem de sobra',
12
+ 'cache: o problema e a solução',
13
+ 'recursão: ver "recursão"',
14
+ 'prod de pé? reza o commit',
15
+ 'TODO: escrever um TODO melhor',
16
+ 'semicolon ; o herói anônimo',
17
+ 'rebase com fé, push com medo',
18
+ 'git blame aponta pra mim de novo',
19
+ 'tabs vs spaces: a guerra eterna',
20
+ 'commit -m "ajustes"',
21
+ 'tava funcionando ontem',
22
+ 'dark mode pra esconder os bugs',
23
+ 'café.exe parou de responder',
24
+ 'compilou? então tá pronto',
25
+ 'é só um if, o que pode dar errado?',
26
+ 'prod é o melhor ambiente de testes',
27
+ 'documentação? o código é a doc',
28
+ 'Stack Overflow, meu copiloto de verdade',
29
+ 'um teste a menos, um deploy a mais',
30
+ 'null: o erro de um bilhão de dólares',
31
+ 'regex: agora você tem 2 problemas',
32
+ 'quem precisa de QA tem usuário',
33
+ 'funcionou de primeira? desconfie',
34
+ 'legacy: código com medo de mexer',
35
+ 'hotfix vira feature permanente',
36
+ '"depois eu refatoro" — ninguém, nunca',
37
+ 'force push, força e fé',
38
+ 'erro 500: culpa do estagiário',
39
+ 'clean code? clean café primeiro',
40
+ 'deadline: substantivo coletivo de pânico',
41
+ 'rollback é o novo deploy',
42
+ 'off by one, off by sanidade',
43
+ 'YAML: os espaços te odeiam',
44
+ 'docker: roda na minha e na sua máquina',
45
+ 'o bug é o teste passando errado',
46
+ 'sexta 17h: não mexe que tá funcionando',
47
+ 'pair programming: dois pra culpar',
48
+ 'microserviço: monólito distribuído',
49
+ 'temporário é o que mais dura',
50
+ ];
package/lib/ui.mjs ADDED
@@ -0,0 +1,111 @@
1
+ // ui.mjs — helpers de terminal pro instalador interativo. Zero dependencias (node:readline).
2
+ // Prompts (multiselect/select/confirm) usam raw mode; sempre restauram o terminal no fim.
3
+ import readline from 'node:readline';
4
+
5
+ const out = process.stdout;
6
+ export const C = {
7
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
8
+ cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', magenta: '\x1b[35m', gray: '\x1b[90m',
9
+ };
10
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ export const hideCursor = () => out.isTTY && out.write('\x1b[?25l');
12
+ export const showCursor = () => out.isTTY && out.write('\x1b[?25h');
13
+
14
+ export const isInteractive = () => !!process.stdin.isTTY && !!process.stdout.isTTY;
15
+
16
+ function withKeypress(onKey, draw) {
17
+ return new Promise((resolve) => {
18
+ readline.emitKeypressEvents(process.stdin);
19
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
20
+ process.stdin.resume();
21
+ hideCursor();
22
+ const cleanup = () => {
23
+ process.stdin.removeListener('keypress', handler);
24
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
25
+ process.stdin.pause();
26
+ showCursor();
27
+ };
28
+ const handler = (str, key) => {
29
+ if (!key) return;
30
+ if (key.ctrl && key.name === 'c') { cleanup(); out.write('\n'); process.exit(130); }
31
+ onKey(key, (result) => { cleanup(); resolve(result); }, draw);
32
+ };
33
+ process.stdin.on('keypress', handler);
34
+ draw(true);
35
+ });
36
+ }
37
+
38
+ // choices: [{ name, value, checked? }] -> retorna array de values marcados
39
+ export function multiselect(message, choices) {
40
+ let idx = 0;
41
+ const sel = choices.map((c) => !!c.checked);
42
+ const render = (first) => {
43
+ const lines = choices.length + 1;
44
+ if (!first) out.write(`\x1b[${lines}A`);
45
+ out.write(`\x1b[0G\x1b[K${C.bold}? ${message}${C.reset} ${C.dim}(↑↓ · espaço marca · a=todos · enter)${C.reset}\n`);
46
+ choices.forEach((c, i) => {
47
+ const ptr = i === idx ? `${C.cyan}❯${C.reset}` : ' ';
48
+ const box = sel[i] ? `${C.green}◉${C.reset}` : `${C.dim}◯${C.reset}`;
49
+ const label = i === idx ? `${C.cyan}${c.name}${C.reset}` : c.name;
50
+ out.write(`\x1b[0G\x1b[K ${ptr} ${box} ${label}\n`);
51
+ });
52
+ };
53
+ return withKeypress((key, done) => {
54
+ if (key.name === 'up') idx = (idx - 1 + choices.length) % choices.length;
55
+ else if (key.name === 'down') idx = (idx + 1) % choices.length;
56
+ else if (key.name === 'space') sel[idx] = !sel[idx];
57
+ else if (key.name === 'a') { const all = sel.every(Boolean); sel.fill(!all); }
58
+ else if (key.name === 'return') return done(choices.filter((_, i) => sel[i]).map((c) => c.value));
59
+ render(false);
60
+ }, render);
61
+ }
62
+
63
+ // choices: [{ name, value }] -> retorna 1 value
64
+ export function select(message, choices, initial = 0) {
65
+ let idx = initial;
66
+ const render = (first) => {
67
+ const lines = choices.length + 1;
68
+ if (!first) out.write(`\x1b[${lines}A`);
69
+ out.write(`\x1b[0G\x1b[K${C.bold}? ${message}${C.reset} ${C.dim}(↑↓ · enter)${C.reset}\n`);
70
+ choices.forEach((c, i) => {
71
+ const ptr = i === idx ? `${C.cyan}❯${C.reset}` : ' ';
72
+ const label = i === idx ? `${C.cyan}${c.name}${C.reset}` : c.name;
73
+ out.write(`\x1b[0G\x1b[K ${ptr} ${label}\n`);
74
+ });
75
+ };
76
+ return withKeypress((key, done) => {
77
+ if (key.name === 'up') idx = (idx - 1 + choices.length) % choices.length;
78
+ else if (key.name === 'down') idx = (idx + 1) % choices.length;
79
+ else if (key.name === 'return') return done(choices[idx].value);
80
+ render(false);
81
+ }, render);
82
+ }
83
+
84
+ export function confirm(message, def = true) {
85
+ const render = (first) => {
86
+ if (!first) out.write('\x1b[1A');
87
+ out.write(`\x1b[0G\x1b[K${C.bold}? ${message}${C.reset} ${C.dim}(${def ? 'Y/n' : 'y/N'})${C.reset}\n`);
88
+ };
89
+ return withKeypress((key, done) => {
90
+ if (key.name === 'y') return done(true);
91
+ if (key.name === 'n') return done(false);
92
+ if (key.name === 'return') return done(def);
93
+ render(false);
94
+ }, render);
95
+ }
96
+
97
+ // spinner com checkmark no fim
98
+ export function spinner(text) {
99
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
100
+ if (!out.isTTY) { out.write(` ${C.dim}…${C.reset} ${text}\n`); return { stop() {} }; }
101
+ hideCursor();
102
+ let i = 0;
103
+ const t = setInterval(() => out.write(`\x1b[0G\x1b[K${C.cyan}${frames[i++ % frames.length]}${C.reset} ${text}`), 80);
104
+ return {
105
+ stop(ok = true, msg) {
106
+ clearInterval(t);
107
+ out.write(`\x1b[0G\x1b[K${ok ? C.green + '✓' : C.red + '✗'}${C.reset} ${msg || text}\n`);
108
+ showCursor();
109
+ },
110
+ };
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moodline",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Statusline divertida e informativa para CLIs de IA (Claude Code, GitHub Copilot CLI e mais): barra de contexto em gradiente, emoji-humor, git, custo e trocadilhos de dev.",
5
5
  "type": "module",
6
6
  "bin": {