ll-skills 1.0.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/CHANGELOG.md +18 -0
- package/README.md +105 -0
- package/agents/ll-implementador.md +23 -0
- package/bin/install.js +475 -0
- package/hooks/ll-skills-check-update.js +157 -0
- package/package.json +38 -0
- package/skills/ll-atualizar/SKILL.md +68 -0
- package/skills/ll-decidir-antes/SKILL.md +81 -0
- package/skills/ll-decidir-antes/referencias/protocolo-entrevista.md +112 -0
- package/skills/ll-decidir-antes/referencias/template-spec.md +238 -0
- package/skills/ll-desarmar/SKILL.md +254 -0
- package/skills/ll-desarmar/referencias/execucao-adversarial.md +217 -0
- package/skills/ll-desarmar/referencias/humanos-e-substitutos.md +116 -0
- package/skills/ll-desarmar/referencias/placar-e-realimentacao.md +140 -0
- package/skills/ll-orquestrar/SKILL.md +100 -0
- package/skills/ll-pesquisar/SKILL.md +159 -0
- package/skills/ll-pesquisar/referencias/frente-de-pesquisa.md +147 -0
- package/skills/ll-pesquisar/referencias/sintese-e-fontes.md +148 -0
- package/skills/ll-pesquisar-mercado/SKILL.md +112 -0
- package/skills/ll-pesquisar-mercado/referencias/dossie.md +375 -0
- package/skills/ll-pesquisar-mercado/referencias/indice-e-fechamento.md +122 -0
- package/skills/ll-pesquisar-mercado/referencias/padroes-de-pesquisa.md +149 -0
- package/skills/ll-verificar-entrega/SKILL.md +73 -0
- package/skills/ll-verificar-entrega/referencias/briefs-auditoria.md +291 -0
- package/skills/ll-voltar-do-futuro/SKILL.md +239 -0
- package/skills/ll-voltar-do-futuro/referencias/anti-padroes-e-fundamentos.md +201 -0
- package/skills/ll-voltar-do-futuro/referencias/vetores-e-testes.md +228 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/). A skill `ll-atualizar` lê este arquivo para mostrar o que mudou entre a versão instalada e a publicada.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Alterado
|
|
8
|
+
|
|
9
|
+
- Distribuição passa de plugin/marketplace para pacote npm: `npx ll-skills@latest` instala skills standalone em `~/.claude/skills/`, sem o prefixo `ll-skills:`.
|
|
10
|
+
- Skills renomeadas para `ll-<nome>` (`ll-pesquisar-mercado`, `ll-voltar-do-futuro`, `ll-desarmar`, `ll-decidir-antes`, `ll-verificar-entrega`, `ll-pesquisar`, `ll-orquestrar`, `ll-atualizar`); agente renomeado para `ll-implementador`.
|
|
11
|
+
- Instalador limpa a instalação anterior: desinstala o plugin legado `ll-skills@ll-skills`, poda arquivos órfãos de instalações standalone anteriores e apaga o cache antigo.
|
|
12
|
+
- Hook de aviso de atualização reescrito em Node; compara a versão instalada com a publicada no npm.
|
|
13
|
+
- `ll-atualizar` passa a rodar `npx --yes ll-skills@latest` e mostra o changelog a partir deste arquivo.
|
|
14
|
+
- Publicação no npm pela CI com Trusted Publishing: tag `vX.Y.Z` dispara verificação (tag × package.json × CHANGELOG), smoke test e `npm publish` com proveniência.
|
|
15
|
+
|
|
16
|
+
### Removido
|
|
17
|
+
|
|
18
|
+
- Manifesto de plugin (`.claude-plugin/`) e `hooks/hooks.json`.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# LL Skills
|
|
2
|
+
|
|
3
|
+
Coleção de skills para [Claude Code](https://claude.com/claude-code) que forma um **pipeline de desenvolvimento orientado a evidência**: da ideia à entrega verificada, com o mínimo de retrabalho e o máximo de decisões tomadas com lastro — antes de custar caro.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
Requer [Node.js](https://nodejs.org) 18+ (o mesmo que o Claude Code já usa).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ll-skills@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
O instalador copia as skills para `~/.claude/skills/ll-*`, o agente para `~/.claude/agents/` e registra um hook de aviso de atualização em `~/.claude/settings.json`. Se houver uma instalação anterior (inclusive o antigo formato de plugin por marketplace), ela é removida na mesma passada. Reinicie o Claude Code ao final.
|
|
14
|
+
|
|
15
|
+
As skills são invocadas automaticamente pelo Claude quando o pedido bate com a description delas, ou manualmente pelo nome (ex.: `/ll-decidir-antes`). Por serem skills standalone, e não de plugin, não carregam o prefixo `ll-skills:`.
|
|
16
|
+
|
|
17
|
+
Outras formas:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx github:allangdy/ll-skills # direto do repositório, sem passar pelo registro npm
|
|
21
|
+
npx ll-skills@latest --local # instala em ./.claude, só para o projeto atual
|
|
22
|
+
npx ll-skills@latest --uninstall # remove tudo que o instalador colocou
|
|
23
|
+
CLAUDE_CONFIG_DIR=/outro/dir npx ll-skills@latest # honra o diretório de configuração alternativo
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Atualizações
|
|
27
|
+
|
|
28
|
+
Cada versão publicada no npm é uma nova versão. O ll-skills **avisa no início da sessão** quando a versão instalada ficou para trás, e `/ll-atualizar` atualiza por dentro do Claude, mostrando o changelog antes de aplicar. Manualmente: `npx ll-skills@latest` de novo.
|
|
29
|
+
|
|
30
|
+
## O fluxo completo
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart LR
|
|
34
|
+
A[ll-pesquisar-mercado] --> B[ll-voltar-do-futuro]
|
|
35
|
+
B --> C[ll-desarmar]
|
|
36
|
+
C -->|placar realimenta o dossiê| A
|
|
37
|
+
C --> D[ll-decidir-antes]
|
|
38
|
+
D --> E[implementação longa autônoma via SPEC.md]
|
|
39
|
+
E --> F[ll-verificar-entrega]
|
|
40
|
+
F -->|falhas viram novas decisões| D
|
|
41
|
+
O[ll-orquestrar]:::trans -.regras transversais.-> A & B & C & D & E & F
|
|
42
|
+
classDef trans stroke-dasharray: 5 5
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Cada etapa produz o insumo da seguinte, mas **toda skill funciona sozinha** — os handoffs são detectados pelos artefatos no repositório (`docs/`, placar, `SPEC.md`), nunca por acoplamento rígido.
|
|
46
|
+
|
|
47
|
+
### Para um projeto novo
|
|
48
|
+
|
|
49
|
+
1. **`ll-pesquisar-mercado`** — antes de qualquer código: dossiê indexado em `docs/` (mercado, dores, concorrentes, preços sem âncora, viabilidade, economia unitária), com força de evidência por linha. Termina com as **decisões em aberto** e as **premissas ordenadas por letalidade**.
|
|
50
|
+
2. **`ll-voltar-do-futuro`** — o premortem: um agente narra do futuro por que o projeto morreu, atacando o que nunca foi medido. Cada falha traz o aviso que já existia, o viés que cegou e o **teste barato com critério de aceite** que a desarma. As premissas do dossiê são metade do insumo.
|
|
51
|
+
3. **`ll-desarmar`** — executa os testes desarmadores e as POCs com aceite pré-registrado ("reprova primeiro") e preenche o **placar**: DESARMADA, CONFIRMADA COM ROTA DE SAÍDA, EM CURSO… Os números medidos realimentam o dossiê.
|
|
52
|
+
4. **`ll-decidir-antes`** — com os riscos desarmados, a entrevista de decisões: perguntas via AskUserQuestion priorizadas por irreversibilidade × impacto (recomendações sempre com lastro — dos mapas do código ou de pesquisa web), consolidadas em **`SPEC.md` + `PROGRESS.md`** com protocolo anti-drift embutido. Decisões já tomadas nas etapas anteriores não são re-perguntadas.
|
|
53
|
+
5. **Implementação longa** — um agente autônomo (horas ou dias) parte do `SPEC.md`, que é autossuficiente: contrato de decisões, critérios verificáveis por comando, marcos, protocolo de escalada. O ll-skills inclui o agente **`ll-implementador`**, que já parte com a `ll-orquestrar` pré-carregada — mas qualquer sessão/agente com a instrução de partida serve.
|
|
54
|
+
6. **`ll-verificar-entrega`** — auditoria de contexto limpo: um verificador que nunca viu o raciocínio da implementação roda os comandos de aceite da SPEC um a um e confere o placar de marcos contra o código real. O auto-relato de agentes degrada em execuções longas; esta etapa é o que transforma "pronto" em pronto.
|
|
55
|
+
|
|
56
|
+
### Para uma feature de um sistema existente
|
|
57
|
+
|
|
58
|
+
O mesmo pipeline, encurtado — `ll-pesquisar-mercado` detecta o modo no enquadramento:
|
|
59
|
+
|
|
60
|
+
1. **`ll-pesquisar-mercado` (modo feature)** — os dados internos entram como fonte de primeira classe (uso real, tickets, churn, pedidos de clientes = preferência revelada), mais gap competitivo da capacidade e impacto em preço/empacotamento. Desfecho: **construir / construir diferente / não construir**.
|
|
61
|
+
2. **`ll-voltar-do-futuro`** — opcional; vale quando a feature é cara, irreversível ou toca contrato de dados.
|
|
62
|
+
3. **`ll-desarmar`** → **`ll-decidir-antes`** → implementação → **`ll-verificar-entrega`**, como no fluxo novo.
|
|
63
|
+
|
|
64
|
+
Para uma correção pequena ou tarefa trivial, nada disso: o pipeline existe para trabalho onde errar estrutura custa caro.
|
|
65
|
+
|
|
66
|
+
### Transversais
|
|
67
|
+
|
|
68
|
+
**`ll-pesquisar`** — pesquisa profunda de qualquer tema (técnica, comparativo de ferramenta, prática nova — ex.: GEO), em qualquer ponto do fluxo. Pesquisadores de contexto limpo com busca web entregam duas camadas: `SINTESE.md` acionável (fatos → backlog APPLY → decisões DISCUSS → gates de medição) e a trilha de evidências por frente com fontes e trechos salvos, para um agente futuro se aprofundar sem refazer a busca. A síntese é a base natural para a entrevista do `ll-decidir-antes` — a invocação da próxima skill é sempre sua.
|
|
69
|
+
|
|
70
|
+
**`ll-orquestrar`** vale em qualquer etapa que use subagentes — mas é na **implementação longa** que ele mais trabalha: é o manual de como o implementador decompõe por fronteiras de contexto, delega, roteia modelos e verifica com contexto limpo durante horas ou dias. Nas demais etapas, rege os pesquisadores, narradores e verificadores que as skills despacham.
|
|
71
|
+
|
|
72
|
+
## Skills
|
|
73
|
+
|
|
74
|
+
| Skill | Etapa | Descrição |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `ll-pesquisar-mercado` | 1 | Dossiê de mercado orientado a decisão — projeto novo ou feature de sistema existente |
|
|
77
|
+
| `ll-voltar-do-futuro` | 2 | Premortem narrado do futuro: falhas com aviso, viés e teste desarmador |
|
|
78
|
+
| `ll-desarmar` | 3 | Executa testes desarmadores e POCs com aceite pré-registrado e preenche o placar |
|
|
79
|
+
| `ll-decidir-antes` | 4 | Entrevista de decisões → SPEC.md + PROGRESS.md para implementação autônoma longa |
|
|
80
|
+
| `ll-verificar-entrega` | 6 | Auditoria de contexto limpo da entrega contra os critérios da SPEC |
|
|
81
|
+
| `ll-pesquisar` | — | Pesquisa profunda de qualquer tema em duas camadas: síntese acionável + trilha de evidências reutilizável |
|
|
82
|
+
| `ll-orquestrar` | — | Regras de orquestração multi-agente: delegação, briefs, roteamento, verificação |
|
|
83
|
+
| `ll-atualizar` | — | Atualiza o ll-skills para a última versão publicada, com o changelog do que mudou antes de aplicar |
|
|
84
|
+
|
|
85
|
+
## Adicionando novas skills
|
|
86
|
+
|
|
87
|
+
1. Crie `skills/ll-<nome>/SKILL.md` com frontmatter `name: ll-<nome>` (kebab-case, igual ao nome da pasta) e `description` (diz ao Claude **quando** invocar)
|
|
88
|
+
2. Material de profundidade vai em `skills/ll-<nome>/referencias/`, lido no momento certo
|
|
89
|
+
3. Atualize a tabela acima e registre a mudança em `CHANGELOG.md`, numa seção `## [x.y.z] - data` com a versão que vai sair
|
|
90
|
+
4. Publique uma versão (abaixo). O hook avisa quem está atrasado na próxima sessão.
|
|
91
|
+
|
|
92
|
+
O instalador (`bin/install.js`) descobre as skills pela pasta `skills/ll-*` e os agentes por `agents/ll-*.md`; não há lista para manter. `npm test` roda o smoke test do instalador num diretório isolado.
|
|
93
|
+
|
|
94
|
+
## Publicando uma versão
|
|
95
|
+
|
|
96
|
+
A publicação no npm é feita pela CI via [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC entre GitHub Actions e npm, sem token guardado em lugar nenhum). Cada tag `vX.Y.Z` dispara `.github/workflows/publish.yml`, que confere tag × `package.json` × `CHANGELOG.md`, roda o smoke test e publica com proveniência.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm version patch|minor|major # sobe package.json, commita e cria a tag vX.Y.Z
|
|
100
|
+
git push --follow-tags # o push da tag dispara a publicação
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Regra de bump: `patch` para ajuste em skill existente, `minor` para skill nova ou mudança de comportamento, `major` para renomear ou remover skill. O workflow falha se o `CHANGELOG.md` não tiver a seção da versão.
|
|
104
|
+
|
|
105
|
+
Configuração feita uma vez no npmjs.com, em *Package settings → Trusted Publisher*: GitHub Actions, user `allangdy`, repository `ll-skills`, workflow `publish.yml`. Em *Publishing access*, "Require two-factor authentication and disallow tokens", para que só a CI publique.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ll-implementador
|
|
3
|
+
description: Implementador autônomo de longa duração que executa um SPEC.md gerado pela skill decidir-antes até todos os marcos estarem verdes. Use para disparar a implementação de uma spec pronta (horas ou dias de execução), passando o caminho do SPEC.md na instrução.
|
|
4
|
+
skills: ["ll-orquestrar"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Você é o implementador de uma especificação de longa duração produzida pela skill
|
|
8
|
+
`ll-decidir-antes`. Sua missão recebe o caminho de um `SPEC.md`; implemente-o até o fim.
|
|
9
|
+
|
|
10
|
+
Leia a spec inteira antes de qualquer código. Ela é autossuficiente: as decisões da seção 3
|
|
11
|
+
são contrato e nada é re-decidido; a seção 7 é o seu protocolo de operação e prevalece sobre
|
|
12
|
+
qualquer hábito ou instrução genérica de sessão. Estado vive em `PROGRESS.md` e no git, não
|
|
13
|
+
na conversa. Trabalhe um marco por vez até todos estarem `passes: true` com os comandos de
|
|
14
|
+
verificação passando nesta sessão.
|
|
15
|
+
|
|
16
|
+
Ao delegar trabalho a subagentes, siga a skill `ll-orquestrar` (pré-carregada neste agente):
|
|
17
|
+
delegue apenas subtarefas grandes e genuinamente independentes, com brief autossuficiente e
|
|
18
|
+
roteamento de modelo por etapa, e verifique resultados de subagentes com evidência barata.
|
|
19
|
+
|
|
20
|
+
Antes de registrar qualquer progresso, audite cada afirmação contra um resultado de
|
|
21
|
+
ferramenta desta sessão — comando rodado, teste executado, arquivo lido. Ao terminar,
|
|
22
|
+
declare o estado real de cada marco; a auditoria final é da skill `ll-verificar-entrega`, e o
|
|
23
|
+
`VERIFICACAO.md` dela, não o seu relato, é o que fecha o trabalho.
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Instalador do ll-skills.
|
|
5
|
+
//
|
|
6
|
+
// npx ll-skills@latest instala/atualiza em ~/.claude (ou $CLAUDE_CONFIG_DIR)
|
|
7
|
+
// npx ll-skills@latest --local instala em ./.claude do diretório atual
|
|
8
|
+
// npx ll-skills@latest --uninstall
|
|
9
|
+
//
|
|
10
|
+
// Copia skills/ll-*, agents/ll-* e o hook de aviso de atualização para o diretório
|
|
11
|
+
// de configuração do Claude Code, registra o hook em settings.json, grava
|
|
12
|
+
// VERSION + manifesto com sha256 por arquivo e limpa o que ficou de instalações
|
|
13
|
+
// anteriores (plugin legado por marketplace, arquivos órfãos, cache antigo).
|
|
14
|
+
//
|
|
15
|
+
// Sem dependências, sem prompts. Tudo relativo ao próprio pacote (__dirname),
|
|
16
|
+
// nunca ao cwd.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
const PKG = require(path.join(PKG_ROOT, 'package.json'));
|
|
26
|
+
const PKG_NAME = PKG.name;
|
|
27
|
+
const REPO_URL = 'https://github.com/allangdy/ll-skills.git';
|
|
28
|
+
const LEGACY_PLUGIN_ID = 'll-skills@ll-skills';
|
|
29
|
+
const LEGACY_MARKETPLACE = 'll-skills';
|
|
30
|
+
const HOOK_FILE = 'll-skills-check-update.js';
|
|
31
|
+
const HOOK_MATCHER = 'startup|resume';
|
|
32
|
+
const PREFIXOS_PROPRIOS = ['skills/ll-', 'agents/ll-', 'hooks/ll-skills-', 'll-skills/'];
|
|
33
|
+
|
|
34
|
+
const CACHE_DIR = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'll-skills');
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// utilitários
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function log(msg) {
|
|
41
|
+
process.stdout.write(msg + '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fail(msg, code = 1) {
|
|
45
|
+
process.stderr.write('ll-skills: ' + msg + '\n');
|
|
46
|
+
process.exit(code);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = { uninstall: false, local: false, help: false };
|
|
51
|
+
for (const a of argv) {
|
|
52
|
+
if (a === '--uninstall') args.uninstall = true;
|
|
53
|
+
else if (a === '--local') args.local = true;
|
|
54
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
55
|
+
else fail(`argumento desconhecido: ${a} (use --help)`);
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function usage() {
|
|
61
|
+
log(`ll-skills ${PKG.version}
|
|
62
|
+
|
|
63
|
+
Uso:
|
|
64
|
+
npx ll-skills@latest instala ou atualiza em $CLAUDE_CONFIG_DIR ou ~/.claude
|
|
65
|
+
npx ll-skills@latest --local instala em ./.claude (só este projeto)
|
|
66
|
+
npx ll-skills@latest --uninstall remove tudo que este pacote instalou
|
|
67
|
+
npx ll-skills@latest --help`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveConfigDir(args) {
|
|
71
|
+
if (args.local) return path.resolve(process.cwd(), '.claude');
|
|
72
|
+
if (process.env.CLAUDE_CONFIG_DIR) return path.resolve(process.env.CLAUDE_CONFIG_DIR);
|
|
73
|
+
return path.join(os.homedir(), '.claude');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sha256(buf) {
|
|
77
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Retorna `fallback` se o arquivo não existe; `null` se existe mas não é JSON válido.
|
|
81
|
+
function readJson(file, fallback) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (e.code === 'ENOENT') return fallback;
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(raw);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeJsonAtomic(file, obj) {
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n');
|
|
100
|
+
fs.renameSync(tmp, file);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function insideDir(base, target) {
|
|
104
|
+
const rel = path.relative(base, target);
|
|
105
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isOwnRel(rel) {
|
|
109
|
+
return PREFIXOS_PROPRIOS.some((p) => rel.startsWith(p));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function walk(dir, base = dir, out = []) {
|
|
113
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
const full = path.join(dir, entry.name);
|
|
115
|
+
if (entry.isDirectory()) walk(full, base, out);
|
|
116
|
+
else if (entry.isFile()) out.push(path.relative(base, full));
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Lista de arquivos do pacote a instalar: [{ src, rel }], rel relativo ao configDir.
|
|
122
|
+
function planFiles(pkgRoot) {
|
|
123
|
+
const plan = [];
|
|
124
|
+
const skillsDir = path.join(pkgRoot, 'skills');
|
|
125
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
126
|
+
if (!name.startsWith('ll-')) continue;
|
|
127
|
+
const dir = path.join(skillsDir, name);
|
|
128
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
129
|
+
for (const rel of walk(dir)) {
|
|
130
|
+
plan.push({ src: path.join(dir, rel), rel: path.posix.join('skills', name, rel.split(path.sep).join('/')) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const agentsDir = path.join(pkgRoot, 'agents');
|
|
134
|
+
if (fs.existsSync(agentsDir)) {
|
|
135
|
+
for (const name of fs.readdirSync(agentsDir)) {
|
|
136
|
+
if (name.startsWith('ll-') && name.endsWith('.md')) {
|
|
137
|
+
plan.push({ src: path.join(agentsDir, name), rel: `agents/${name}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const hooksDir = path.join(pkgRoot, 'hooks');
|
|
142
|
+
if (fs.existsSync(hooksDir)) {
|
|
143
|
+
for (const name of fs.readdirSync(hooksDir)) {
|
|
144
|
+
if (name.startsWith('ll-skills-') && name.endsWith('.js')) {
|
|
145
|
+
plan.push({ src: path.join(hooksDir, name), rel: `hooks/${name}`, mode: 0o755 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return plan;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// origem da instalação (registry npm, github: ou clone local)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
function detectInstallSource(pkgRoot) {
|
|
157
|
+
const npxRoot = path.resolve(pkgRoot, '..', '..');
|
|
158
|
+
const key = `node_modules/${PKG_NAME}`;
|
|
159
|
+
const lock =
|
|
160
|
+
readJson(path.join(npxRoot, 'node_modules', '.package-lock.json'), null) ||
|
|
161
|
+
readJson(path.join(npxRoot, 'package-lock.json'), null);
|
|
162
|
+
const resolved = (lock && lock.packages && lock.packages[key] && lock.packages[key].resolved) || '';
|
|
163
|
+
if (/^git\+/.test(resolved)) {
|
|
164
|
+
return { source: 'github', resolved, sha: resolved.split('#')[1] || null };
|
|
165
|
+
}
|
|
166
|
+
if (/registry\.npmjs\.org/.test(resolved)) {
|
|
167
|
+
return { source: 'registry', resolved, sha: null };
|
|
168
|
+
}
|
|
169
|
+
const outer = readJson(path.join(npxRoot, 'package.json'), null);
|
|
170
|
+
const pkgs = (outer && outer._npx && outer._npx.packages) || [];
|
|
171
|
+
const gh = pkgs.find((p) => /^(github:|git\+|git:)/.test(p));
|
|
172
|
+
if (gh) return { source: 'github', resolved: gh, sha: null };
|
|
173
|
+
if (fs.existsSync(path.join(pkgRoot, '.git'))) {
|
|
174
|
+
const r = spawnSync('git', ['-C', pkgRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
175
|
+
const sha = r.status === 0 ? r.stdout.trim() : null;
|
|
176
|
+
return { source: 'local', resolved: pkgRoot, sha };
|
|
177
|
+
}
|
|
178
|
+
return { source: 'local', resolved: pkgRoot, sha: null };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// plugin legado (marketplace)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
function removeLegacyPlugin(configDir) {
|
|
186
|
+
const registry = readJson(path.join(configDir, 'plugins', 'installed_plugins.json'), {});
|
|
187
|
+
const entries = registry && registry.plugins && registry.plugins[LEGACY_PLUGIN_ID];
|
|
188
|
+
if (!Array.isArray(entries) || entries.length === 0) return { found: false, removed: false, manual: [] };
|
|
189
|
+
|
|
190
|
+
const cmds = [];
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
const scope = e.scope || 'user';
|
|
193
|
+
const cwd = scope === 'project' && e.projectPath ? e.projectPath : process.cwd();
|
|
194
|
+
cmds.push({ args: ['plugin', 'uninstall', LEGACY_PLUGIN_ID, '-s', scope], cwd });
|
|
195
|
+
}
|
|
196
|
+
cmds.push({ args: ['plugin', 'marketplace', 'remove', LEGACY_MARKETPLACE], cwd: process.cwd() });
|
|
197
|
+
|
|
198
|
+
const manual = [];
|
|
199
|
+
let cliMissing = false;
|
|
200
|
+
for (const c of cmds) {
|
|
201
|
+
if (cliMissing) {
|
|
202
|
+
manual.push(`claude ${c.args.join(' ')}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const r = spawnSync('claude', c.args, { cwd: c.cwd, stdio: 'pipe', encoding: 'utf8', timeout: 60000 });
|
|
206
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
207
|
+
cliMissing = true;
|
|
208
|
+
manual.push(`claude ${c.args.join(' ')}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Falhas de "já removido" são esperadas e ignoradas.
|
|
212
|
+
}
|
|
213
|
+
return { found: true, removed: !cliMissing, manual };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// cópia, poda e estado
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
function copyFiles(configDir, plan) {
|
|
221
|
+
const files = {};
|
|
222
|
+
for (const item of plan) {
|
|
223
|
+
const dest = path.join(configDir, item.rel);
|
|
224
|
+
try {
|
|
225
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
226
|
+
const buf = fs.readFileSync(item.src);
|
|
227
|
+
fs.writeFileSync(dest, buf);
|
|
228
|
+
if (item.mode) fs.chmodSync(dest, item.mode);
|
|
229
|
+
files[item.rel] = sha256(buf);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
fail(`não consegui gravar ${dest}: ${e.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return files;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function removeEmptyDirsUpTo(dir, stopAt) {
|
|
238
|
+
let cur = dir;
|
|
239
|
+
while (insideDir(stopAt, cur)) {
|
|
240
|
+
try {
|
|
241
|
+
if (fs.readdirSync(cur).length > 0) return;
|
|
242
|
+
fs.rmdirSync(cur);
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
cur = path.dirname(cur);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function removeManagedFile(configDir, rel) {
|
|
251
|
+
if (!isOwnRel(rel)) return false;
|
|
252
|
+
const full = path.resolve(configDir, rel);
|
|
253
|
+
if (!insideDir(configDir, full)) return false;
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(full);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
if (e.code !== 'ENOENT') return false;
|
|
258
|
+
}
|
|
259
|
+
removeEmptyDirsUpTo(path.dirname(full), configDir);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function pruneStale(configDir, oldManifest, newFiles) {
|
|
264
|
+
const removed = [];
|
|
265
|
+
const old = (oldManifest && oldManifest.files) || {};
|
|
266
|
+
for (const rel of Object.keys(old)) {
|
|
267
|
+
if (rel in newFiles) continue;
|
|
268
|
+
if (removeManagedFile(configDir, rel)) removed.push(rel);
|
|
269
|
+
}
|
|
270
|
+
return removed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function stateDir(configDir) {
|
|
274
|
+
return path.join(configDir, 'll-skills');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeState(configDir, files, sourceInfo) {
|
|
278
|
+
const dir = stateDir(configDir);
|
|
279
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
280
|
+
fs.writeFileSync(path.join(dir, 'VERSION'), PKG.version + '\n');
|
|
281
|
+
writeJsonAtomic(path.join(dir, 'manifest.json'), {
|
|
282
|
+
version: PKG.version,
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
files,
|
|
285
|
+
});
|
|
286
|
+
writeJsonAtomic(path.join(dir, 'install.json'), {
|
|
287
|
+
version: PKG.version,
|
|
288
|
+
installedAt: new Date().toISOString(),
|
|
289
|
+
repo: REPO_URL,
|
|
290
|
+
...sourceInfo,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clearOldCache() {
|
|
295
|
+
try {
|
|
296
|
+
fs.rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
297
|
+
} catch {
|
|
298
|
+
/* melhor esforço */
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// settings.json
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
function hookCommand(configDir) {
|
|
307
|
+
const hookPath = path.join(configDir, 'hooks', HOOK_FILE);
|
|
308
|
+
// `node` bare: com nvm, um caminho absoluto para o binário quebra no próximo `nvm install`.
|
|
309
|
+
return `command -v node >/dev/null 2>&1 && node "${hookPath}" || true`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildHookEntry(configDir) {
|
|
313
|
+
return {
|
|
314
|
+
matcher: HOOK_MATCHER,
|
|
315
|
+
hooks: [{ type: 'command', command: hookCommand(configDir), timeout: 5 }],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isOwnHook(h) {
|
|
320
|
+
return Boolean(h && typeof h.command === 'string' && h.command.includes(HOOK_FILE));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stripOwnHooks(settings) {
|
|
324
|
+
const ss = settings.hooks && settings.hooks.SessionStart;
|
|
325
|
+
if (!Array.isArray(ss)) return;
|
|
326
|
+
settings.hooks.SessionStart = ss
|
|
327
|
+
.map((e) => (e && Array.isArray(e.hooks) ? { ...e, hooks: e.hooks.filter((h) => !isOwnHook(h)) } : e))
|
|
328
|
+
.filter((e) => !(e && Array.isArray(e.hooks) && e.hooks.length === 0));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function stripLegacyPluginKeys(settings) {
|
|
332
|
+
if (settings.enabledPlugins && Object.prototype.hasOwnProperty.call(settings.enabledPlugins, LEGACY_PLUGIN_ID)) {
|
|
333
|
+
delete settings.enabledPlugins[LEGACY_PLUGIN_ID];
|
|
334
|
+
if (Object.keys(settings.enabledPlugins).length === 0) delete settings.enabledPlugins;
|
|
335
|
+
}
|
|
336
|
+
if (settings.extraKnownMarketplaces && Object.prototype.hasOwnProperty.call(settings.extraKnownMarketplaces, LEGACY_MARKETPLACE)) {
|
|
337
|
+
delete settings.extraKnownMarketplaces[LEGACY_MARKETPLACE];
|
|
338
|
+
if (Object.keys(settings.extraKnownMarketplaces).length === 0) delete settings.extraKnownMarketplaces;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function pruneEmptyHooks(settings) {
|
|
343
|
+
if (settings.hooks && Array.isArray(settings.hooks.SessionStart) && settings.hooks.SessionStart.length === 0) {
|
|
344
|
+
delete settings.hooks.SessionStart;
|
|
345
|
+
}
|
|
346
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Lê, aplica `mutate(settings)`, grava só se mudou. Retorna 'unchanged' | 'written' | 'invalid'.
|
|
350
|
+
function updateSettings(configDir, mutate) {
|
|
351
|
+
const file = path.join(configDir, 'settings.json');
|
|
352
|
+
const settings = readJson(file, {});
|
|
353
|
+
if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) return 'invalid';
|
|
354
|
+
const before = JSON.stringify(settings);
|
|
355
|
+
mutate(settings);
|
|
356
|
+
if (JSON.stringify(settings) === before) return 'unchanged';
|
|
357
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.ll-skills.bak`);
|
|
358
|
+
writeJsonAtomic(file, settings);
|
|
359
|
+
return 'written';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function registerHook(configDir) {
|
|
363
|
+
return updateSettings(configDir, (s) => {
|
|
364
|
+
if (!s.hooks || typeof s.hooks !== 'object') s.hooks = {};
|
|
365
|
+
if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
|
|
366
|
+
stripOwnHooks(s);
|
|
367
|
+
s.hooks.SessionStart.push(buildHookEntry(configDir));
|
|
368
|
+
stripLegacyPluginKeys(s);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function unregisterHook(configDir) {
|
|
373
|
+
return updateSettings(configDir, (s) => {
|
|
374
|
+
stripOwnHooks(s);
|
|
375
|
+
pruneEmptyHooks(s);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function printManualHookSnippet(configDir) {
|
|
380
|
+
log('');
|
|
381
|
+
log(`AVISO: ${path.join(configDir, 'settings.json')} não é um JSON válido; não foi alterado.`);
|
|
382
|
+
log('Corrija o arquivo e adicione manualmente em "hooks" > "SessionStart":');
|
|
383
|
+
log('');
|
|
384
|
+
log(JSON.stringify(buildHookEntry(configDir), null, 2));
|
|
385
|
+
log('');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// fluxos
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
function install(args) {
|
|
393
|
+
const configDir = resolveConfigDir(args);
|
|
394
|
+
log(`ll-skills ${PKG.version} → ${configDir}`);
|
|
395
|
+
|
|
396
|
+
// 1. plugin legado, ANTES de tocar em settings.json (o CLI `claude` reescreve o arquivo).
|
|
397
|
+
const legacy = removeLegacyPlugin(configDir);
|
|
398
|
+
if (legacy.found) {
|
|
399
|
+
if (legacy.removed) log('• plugin legado ll-skills@ll-skills desinstalado (marketplace removido)');
|
|
400
|
+
else log('• plugin legado detectado, mas o CLI `claude` não está no PATH');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 2. instalação standalone anterior
|
|
404
|
+
const oldManifest = readJson(path.join(stateDir(configDir), 'manifest.json'), null);
|
|
405
|
+
|
|
406
|
+
// 3. cópia
|
|
407
|
+
const plan = planFiles(PKG_ROOT);
|
|
408
|
+
const files = copyFiles(configDir, plan);
|
|
409
|
+
const skillNames = [...new Set(Object.keys(files).filter((r) => r.startsWith('skills/')).map((r) => r.split('/')[1]))].sort();
|
|
410
|
+
const agentNames = Object.keys(files).filter((r) => r.startsWith('agents/')).map((r) => path.basename(r, '.md'));
|
|
411
|
+
log(`• ${skillNames.length} skills: ${skillNames.join(', ')}`);
|
|
412
|
+
if (agentNames.length) log(`• ${agentNames.length} agente: ${agentNames.join(', ')}`);
|
|
413
|
+
|
|
414
|
+
// 4. poda
|
|
415
|
+
const pruned = pruneStale(configDir, oldManifest, files);
|
|
416
|
+
if (pruned.length) log(`• ${pruned.length} arquivo(s) órfão(s) removido(s) da instalação anterior`);
|
|
417
|
+
|
|
418
|
+
// 5. estado
|
|
419
|
+
writeState(configDir, files, detectInstallSource(PKG_ROOT));
|
|
420
|
+
|
|
421
|
+
// 6. hook
|
|
422
|
+
const hookResult = registerHook(configDir);
|
|
423
|
+
if (hookResult === 'written') log('• hook de aviso de atualização registrado em settings.json');
|
|
424
|
+
else if (hookResult === 'unchanged') log('• hook de aviso de atualização já registrado');
|
|
425
|
+
|
|
426
|
+
// 7. cache antigo
|
|
427
|
+
clearOldCache();
|
|
428
|
+
|
|
429
|
+
log('');
|
|
430
|
+
if (legacy.manual.length) {
|
|
431
|
+
log('Para concluir a remoção do plugin legado, rode dentro do Claude Code ou no terminal:');
|
|
432
|
+
for (const c of legacy.manual) log(` ${c}`);
|
|
433
|
+
log('');
|
|
434
|
+
}
|
|
435
|
+
if (hookResult === 'invalid') {
|
|
436
|
+
printManualHookSnippet(configDir);
|
|
437
|
+
log('Reinicie o Claude Code para que as skills ll-* apareçam.');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
log('Pronto. Reinicie o Claude Code para que as skills ll-* apareçam (ex.: /ll-decidir-antes).');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function uninstall(args) {
|
|
444
|
+
const configDir = resolveConfigDir(args);
|
|
445
|
+
log(`ll-skills --uninstall → ${configDir}`);
|
|
446
|
+
|
|
447
|
+
const manifest = readJson(path.join(stateDir(configDir), 'manifest.json'), null);
|
|
448
|
+
const rels = manifest && manifest.files ? Object.keys(manifest.files) : planFiles(PKG_ROOT).map((p) => p.rel);
|
|
449
|
+
let n = 0;
|
|
450
|
+
for (const rel of rels) if (removeManagedFile(configDir, rel)) n++;
|
|
451
|
+
log(`• ${n} arquivo(s) removido(s)`);
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
fs.rmSync(stateDir(configDir), { recursive: true, force: true });
|
|
455
|
+
} catch {
|
|
456
|
+
/* melhor esforço */
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const hookResult = unregisterHook(configDir);
|
|
460
|
+
if (hookResult === 'written') log('• hook removido de settings.json');
|
|
461
|
+
else if (hookResult === 'invalid') log(`• AVISO: settings.json inválido; remova o hook "${HOOK_FILE}" manualmente`);
|
|
462
|
+
|
|
463
|
+
clearOldCache();
|
|
464
|
+
log('');
|
|
465
|
+
log('ll-skills removido. Reinicie o Claude Code.');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function main() {
|
|
469
|
+
const args = parseArgs(process.argv.slice(2));
|
|
470
|
+
if (args.help) return usage();
|
|
471
|
+
if (args.uninstall) return uninstall(args);
|
|
472
|
+
return install(args);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
main();
|