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 +32 -7
- package/bin/moodline.js +128 -172
- package/lib/install.mjs +99 -0
- package/lib/logo.mjs +82 -0
- package/lib/moodline-core.mjs +9 -25
- package/lib/puns.mjs +50 -0
- package/lib/ui.mjs +111 -0
- package/package.json +1 -1
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.
|
|
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 (
|
|
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
|
|
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
|
|
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/
|
|
2
|
+
// moodline — instalador/CLI. Escopo de instalacao SEMPRE global (user-level).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// moodline
|
|
6
|
-
// moodline
|
|
7
|
-
// moodline doctor
|
|
8
|
-
// moodline uninstall
|
|
9
|
-
// moodline
|
|
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 {
|
|
13
|
-
import { homedir } from 'node:os';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { join, dirname } from 'node:path';
|
|
15
|
-
import { fileURLToPath
|
|
16
|
-
import { render, ADAPTERS,
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
77
|
+
postInstall(keys);
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
console.log();
|
|
124
|
-
|
|
125
|
-
|
|
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 =
|
|
136
|
-
|
|
137
|
-
for (const
|
|
138
|
-
const t =
|
|
139
|
-
|
|
140
|
-
|
|
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(`${
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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(`${
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
${C.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
--
|
|
220
|
-
--
|
|
221
|
-
--
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 só 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
|
-
|
|
231
|
-
const
|
|
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
|
-
|
|
243
|
-
default: cmdHelp(); break;
|
|
199
|
+
default: cmdHelp();
|
|
244
200
|
}
|
package/lib/install.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/moodline-core.mjs
CHANGED
|
@@ -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 (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
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": {
|