@tavaressan/vetor 0.1.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 +42 -0
- package/bin/vetor.js +6 -0
- package/lib/banner.js +35 -0
- package/lib/commands/install.js +71 -0
- package/lib/commands/status.js +59 -0
- package/lib/commands/uninstall.js +119 -0
- package/lib/commands/update.js +63 -0
- package/lib/installer/command-exists.js +30 -0
- package/lib/installer/cursor-hooks.js +181 -0
- package/lib/installer/detector.js +79 -0
- package/lib/installer/manifest.js +76 -0
- package/lib/installer/prompts.js +97 -0
- package/lib/installer/writer.js +382 -0
- package/lib/router.js +50 -0
- package/package.json +39 -0
- package/templates/.gitkeep +0 -0
- package/templates/agents/code-review/agent.json +27 -0
- package/templates/agents/code-review/codex.toml +37 -0
- package/templates/agents/code-review.md +99 -0
- package/templates/agents/issue-worker/agent.json +33 -0
- package/templates/agents/issue-worker/codex.toml +57 -0
- package/templates/agents/issue-worker.md +112 -0
- package/templates/hooks/hooks-codex.json +48 -0
- package/templates/hooks/hooks.json +62 -0
- package/templates/opencode/agent/code-review.md +73 -0
- package/templates/opencode/agent/issue-coordinator.md +521 -0
- package/templates/opencode/agent/issue-worker.md +64 -0
- package/templates/opencode/mcp.jsonc +39 -0
- package/templates/opencode/plugin/vetor.ts +207 -0
- package/templates/opencode/scripts/agent-registration_test.ts +92 -0
- package/templates/opencode/scripts/check-edit.ts +147 -0
- package/templates/opencode/scripts/ensure-external-directory-permission.ts +110 -0
- package/templates/opencode/scripts/ensure-external-directory-permission_test.ts +142 -0
- package/templates/opencode/scripts/lib/guard.ts +45 -0
- package/templates/opencode/scripts/lib/model-health.ts +133 -0
- package/templates/opencode/scripts/lib/model-health_test.ts +181 -0
- package/templates/opencode/scripts/lib/project.ts +240 -0
- package/templates/opencode/scripts/lib/project_test.ts +45 -0
- package/templates/opencode/scripts/lib/status.ts +69 -0
- package/templates/opencode/scripts/lib/worktree.ts +41 -0
- package/templates/opencode/scripts/model-health.ts +50 -0
- package/templates/opencode/scripts/model-health_test.ts +80 -0
- package/templates/opencode/scripts/resolve-model.ts +112 -0
- package/templates/opencode/scripts/resolve-model_test.ts +185 -0
- package/templates/opencode/scripts/safety-check.ts +203 -0
- package/templates/opencode/scripts/vetor-checks.sh +217 -0
- package/templates/opencode/scripts/vetor-status.sh +99 -0
- package/templates/skills/architecture-review/SKILL.md +187 -0
- package/templates/skills/backlog-ideator/SKILL.md +277 -0
- package/templates/skills/design/SKILL.md +468 -0
- package/templates/skills/design/examples/design-contract-example.md +46 -0
- package/templates/skills/design/examples/prototype-handoff-example.md +142 -0
- package/templates/skills/fix-loop-agent/SKILL.md +255 -0
- package/templates/skills/guardian/SKILL.md +343 -0
- package/templates/skills/issue-coordinator/SKILL.md +596 -0
- package/templates/skills/retro/SKILL.md +156 -0
- package/templates/skills/shared/references/agent-status.template.md +68 -0
- package/templates/skills/shared/references/codebase-design-vocabulary.md +54 -0
- package/templates/skills/shared/references/conflict-resolution.md +94 -0
- package/templates/skills/shared/references/delegate-to-runtime.md +239 -0
- package/templates/skills/shared/references/design-vocabulary.md +508 -0
- package/templates/skills/shared/references/evidence-state.md +365 -0
- package/templates/skills/shared/references/frontend-design-enforcement.md +33 -0
- package/templates/skills/shared/references/grilling-conventions.md +64 -0
- package/templates/skills/shared/references/knowledge-provider-contract.md +150 -0
- package/templates/skills/shared/references/mcp-availability.md +104 -0
- package/templates/skills/shared/references/module-test-map.template.md +72 -0
- package/templates/skills/shared/references/planning-conventions.md +97 -0
- package/templates/skills/shared/references/project-conventions.md +63 -0
- package/templates/skills/shared/references/tdd-conventions.md +81 -0
- package/templates/skills/shared/references/touched-files-cache.md +30 -0
- package/templates/skills/spec/SKILL.md +524 -0
- package/templates/skills/spec-validate/SKILL.md +195 -0
- package/templates/skills/spec-validate/references/traceability.md +169 -0
- package/templates/skills/stack-practices/SKILL.md +151 -0
- package/templates/skills/vetor/SKILL.md +174 -0
- package/templates/skills/worktree-create/SKILL.md +142 -0
- package/templates/skills/worktree-ship/SKILL.md +394 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Vetor
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<picture>
|
|
5
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Tavaressan/Vetor/master/assets/logo-dark.png">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/Tavaressan/Vetor/master/assets/logo.png" alt="Vetor Logo" width="320" />
|
|
7
|
+
</picture>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/@tavaressan/vetor)
|
|
11
|
+
[](https://github.com/Tavaressan/Vetor/blob/master/LICENSE)
|
|
12
|
+
|
|
13
|
+
Instalador do **Vetor**: plugin de skills para automação de workflow de desenvolvimento no Claude Code, Codex e outras engines de agente.
|
|
14
|
+
|
|
15
|
+
Este pacote é o instalador de linha de comando. As skills em si (ideação, coordenação de issues, fix loop, ship, guardian) vivem no [repositório principal](https://github.com/Tavaressan/Vetor) — leia o README de lá para o que o Vetor faz.
|
|
16
|
+
|
|
17
|
+
## Instalação
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install -g @tavaressan/vetor
|
|
21
|
+
vetor install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Pré-requisitos:** [Deno](https://deno.com) e `gh` CLI autenticado no PATH, Git com suporte a `git worktree`.
|
|
25
|
+
|
|
26
|
+
## Uso
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
vetor install # instala o Vetor no projeto atual
|
|
30
|
+
vetor update # sincroniza a instalação existente com a fonte
|
|
31
|
+
vetor status # mostra o status da instalação por engine
|
|
32
|
+
vetor uninstall # remove os arquivos instalados pelo Vetor (pede confirmação)
|
|
33
|
+
vetor --help # lista os comandos disponíveis
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Documentação
|
|
37
|
+
|
|
38
|
+
Skills, arquitetura, configuração e compatibilidade multi-engine: [wiki do repositório principal](https://github.com/Tavaressan/Vetor/tree/master/wiki).
|
|
39
|
+
|
|
40
|
+
## Licença
|
|
41
|
+
|
|
42
|
+
[MIT](https://github.com/Tavaressan/Vetor/blob/master/LICENSE) © 2026 Vitor Tavares Chaves.
|
package/bin/vetor.js
ADDED
package/lib/banner.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LOGO_LINES = [
|
|
4
|
+
'██╗ ██╗███████╗████████╗ ██████╗ ██████╗',
|
|
5
|
+
'██║ ██║██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗',
|
|
6
|
+
'██║ ██║█████╗ ██║ ██║ ██║██████╔╝',
|
|
7
|
+
'╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██╔══██╗',
|
|
8
|
+
' ╚████╔╝ ███████╗ ██║ ╚██████╔╝██║ ██║',
|
|
9
|
+
' ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const LOGO_COLOR = '\x1b[36m';
|
|
13
|
+
const RESET = '\x1b[0m';
|
|
14
|
+
|
|
15
|
+
// Só aplica cor quando stdout é um terminal e a convenção NO_COLOR
|
|
16
|
+
// (https://no-color.org/) não está definida. Isso evita poluir saídas
|
|
17
|
+
// redirecionadas para arquivo/pipe (CI, `vetor --help > log`).
|
|
18
|
+
function shouldUseColor() {
|
|
19
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printBanner() {
|
|
23
|
+
const useColor = shouldUseColor();
|
|
24
|
+
|
|
25
|
+
for (const line of LOGO_LINES) {
|
|
26
|
+
console.info(useColor ? `${LOGO_COLOR}${line}${RESET}` : line);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.info('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
printBanner,
|
|
34
|
+
LOGO_LINES,
|
|
35
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { detectEngines: defaultDetectEngines } = require('../installer/detector.js');
|
|
4
|
+
const { runInstallPrompts: defaultRunInstallPrompts } = require('../installer/prompts.js');
|
|
5
|
+
const { installFiles: defaultInstallFiles } = require('../installer/writer.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Comando `install`: detecta engines suportadas no projeto-alvo (ver `ENGINES` em
|
|
9
|
+
* `installer/detector.js`) e oferece seleção interativa
|
|
10
|
+
* com as engines detectadas pré-marcadas. A detecção é só sugestão inicial: nenhuma
|
|
11
|
+
* engine é instalada sem confirmação explícita do usuário via `runInstallPrompts`.
|
|
12
|
+
*
|
|
13
|
+
* Após a confirmação, copia `skills/`/`agents/`/`hooks/` (ou a árvore nativa da engine,
|
|
14
|
+
* quando existir) para o destino de cada engine selecionada via `installFiles` (writer,
|
|
15
|
+
* issue #255, tradução de formato por engine revisada na #283), gravando manifesto de hash
|
|
16
|
+
* por arquivo para updates seguros mais tarde. Engine sem destino de arquivo confirmado
|
|
17
|
+
* (`enginesSkipped`) é reportada ao usuário em vez de silenciosamente ignorada.
|
|
18
|
+
*
|
|
19
|
+
* `detectEngines`/`runInstallPrompts`/`installFiles`/`input`/`output` são injetáveis para
|
|
20
|
+
* testes.
|
|
21
|
+
*/
|
|
22
|
+
async function install(cwd = process.cwd(), options = {}) {
|
|
23
|
+
const detectEngines = options.detectEngines ?? defaultDetectEngines;
|
|
24
|
+
const runInstallPrompts = options.runInstallPrompts ?? defaultRunInstallPrompts;
|
|
25
|
+
const installFiles = options.installFiles ?? defaultInstallFiles;
|
|
26
|
+
const input = options.input ?? process.stdin;
|
|
27
|
+
const output = options.output ?? process.stdout;
|
|
28
|
+
|
|
29
|
+
const engines = detectEngines(cwd);
|
|
30
|
+
const detectedNames = engines.filter((engine) => engine.detected).map((engine) => engine.name);
|
|
31
|
+
|
|
32
|
+
if (detectedNames.length > 0) {
|
|
33
|
+
console.info(`Engines detectadas: ${detectedNames.join(', ')}.`);
|
|
34
|
+
} else {
|
|
35
|
+
console.info('Nenhuma engine detectada no diretório atual.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const selected = await runInstallPrompts(engines, { input, output });
|
|
39
|
+
|
|
40
|
+
if (selected.length === 0) {
|
|
41
|
+
console.info('Nenhuma engine selecionada. Instalação cancelada.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.info(`Engines selecionadas: ${selected.map((engine) => engine.name).join(', ')}.`);
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
copied,
|
|
49
|
+
skipped,
|
|
50
|
+
warnings = [],
|
|
51
|
+
enginesSkipped = [],
|
|
52
|
+
} = installFiles({ projectRoot: cwd, engines: selected });
|
|
53
|
+
console.info(`${copied.length} arquivo(s) copiado(s).`);
|
|
54
|
+
if (skipped.length > 0) {
|
|
55
|
+
console.info(
|
|
56
|
+
`${skipped.length} arquivo(s) não sobrescrito(s) (editado(s) pelo usuário ou não gerado(s) pelo instalador).`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
for (const warning of warnings) {
|
|
60
|
+
console.info(`Aviso: ${warning}`);
|
|
61
|
+
}
|
|
62
|
+
// Issue #283: engine selecionada sem destino de arquivo confirmado (ex.: Antigravity) não
|
|
63
|
+
// falha nem copia nada — mas precisa ser visível para o usuário, não silenciosa.
|
|
64
|
+
for (const engine of enginesSkipped) {
|
|
65
|
+
console.info(
|
|
66
|
+
`${engine.name}: nenhum arquivo instalado (sem convenção de projeto confirmada para esta engine ainda).`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { install };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { ENGINES } = require('../installer/detector.js');
|
|
7
|
+
const { readManifest: defaultReadManifest, hashFile: defaultHashFile } = require('../installer/manifest.js');
|
|
8
|
+
|
|
9
|
+
const ENGINE_NAME_BY_ID = new Map(ENGINES.map((engine) => [engine.id, engine.name]));
|
|
10
|
+
|
|
11
|
+
const STATE_LABEL = {
|
|
12
|
+
ok: '[ok]',
|
|
13
|
+
missing: '[ausente]',
|
|
14
|
+
modified: '[modificado]',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function classify(cwd, key, entry, hashFile) {
|
|
18
|
+
const filePath = path.join(cwd, ...key.split('/'));
|
|
19
|
+
if (!fs.existsSync(filePath)) return 'missing';
|
|
20
|
+
return hashFile(filePath) === entry.sha256 ? 'ok' : 'modified';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Comando `status` (issue #302): classifica cada arquivo do manifesto de instalação
|
|
25
|
+
* (`.vetor/install-manifest.json`) comparando o hash atual em disco com o hash gravado —
|
|
26
|
+
* `ok` (íntegro), `modificado` (hash diverge do manifesto) ou `ausente` (arquivo sumiu do
|
|
27
|
+
* disco). Somente leitura: nenhum arquivo é criado, alterado ou apagado.
|
|
28
|
+
*
|
|
29
|
+
* `readManifest`/`hashFile` são injetáveis para testes.
|
|
30
|
+
*/
|
|
31
|
+
function status(cwd = process.cwd(), options = {}) {
|
|
32
|
+
const readManifest = options.readManifest ?? defaultReadManifest;
|
|
33
|
+
const hashFile = options.hashFile ?? defaultHashFile;
|
|
34
|
+
|
|
35
|
+
const manifest = readManifest(cwd);
|
|
36
|
+
const entries = Object.entries(manifest.files);
|
|
37
|
+
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
console.info('Vetor não está instalado neste projeto.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const byEngine = new Map();
|
|
44
|
+
for (const [key, entry] of entries) {
|
|
45
|
+
const engineName = ENGINE_NAME_BY_ID.get(entry.engine) ?? entry.engine;
|
|
46
|
+
const state = classify(cwd, key, entry, hashFile);
|
|
47
|
+
if (!byEngine.has(engineName)) byEngine.set(engineName, []);
|
|
48
|
+
byEngine.get(engineName).push({ key, state });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [engineName, files] of byEngine) {
|
|
52
|
+
console.info(`${engineName}:`);
|
|
53
|
+
for (const { key, state } of files) {
|
|
54
|
+
console.info(` ${STATE_LABEL[state]} ${key}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { status };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const readline = require('node:readline');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
manifestPathFor,
|
|
9
|
+
readManifest: defaultReadManifest,
|
|
10
|
+
writeManifest: defaultWriteManifest,
|
|
11
|
+
hashFile: defaultHashFile,
|
|
12
|
+
} = require('../installer/manifest.js');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Confirmação simples (s/N) antes da remoção. Sessão não-interativa (sem TTY) não tem como
|
|
16
|
+
* obter confirmação explícita — cancela por padrão, mesma postura de `runInstallPrompts`
|
|
17
|
+
* (installer/prompts.js) para seleção vazia em ambiente não-interativo.
|
|
18
|
+
*/
|
|
19
|
+
function confirmUninstall(message, { input = process.stdin, output = process.stdout, isTTY } = {}) {
|
|
20
|
+
const interactive = isTTY ?? input.isTTY ?? false;
|
|
21
|
+
|
|
22
|
+
if (!interactive) {
|
|
23
|
+
output.write(`${message} Sessão não-interativa: cancelado (confirmação explícita é obrigatória).\n`);
|
|
24
|
+
return Promise.resolve(false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rl = readline.createInterface({ input, output });
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
rl.question(`${message} [s/N]: `, (answer) => {
|
|
30
|
+
rl.close();
|
|
31
|
+
resolve(answer.trim().toLowerCase() === 's');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Comando `uninstall` (issue #303): remove do projeto-alvo os arquivos gerenciados pelo
|
|
38
|
+
* manifesto de instalação (`.vetor/install-manifest.json`) — única operação destrutiva dos
|
|
39
|
+
* três comandos pendentes de `router.js`, por isso pede confirmação antes de apagar algo.
|
|
40
|
+
*
|
|
41
|
+
* Só remove entrada cujo hash atual em disco ainda bate com o manifestado (não editada pelo
|
|
42
|
+
* usuário desde a instalação). Entrada com hash divergente, ou cujo path resolvido cai fora
|
|
43
|
+
* de `projectRoot` (manifesto corrompido/adulterado), é preservada — nunca apagada
|
|
44
|
+
* silenciosamente — e continua registrada no manifesto. Entrada já ausente do disco é só
|
|
45
|
+
* descartada do manifesto (nada físico para preservar).
|
|
46
|
+
*
|
|
47
|
+
* Se nenhuma entrada sobrar após a rodada, o próprio arquivo de manifesto é removido — não
|
|
48
|
+
* um manifesto com `files: {}` — para que `status`/`update` voltem a reportar "não
|
|
49
|
+
* instalado" em vez de uma instalação vazia.
|
|
50
|
+
*
|
|
51
|
+
* `readManifest`/`writeManifest`/`hashFile`/`confirm`/`input`/`output` são injetáveis para
|
|
52
|
+
* testes.
|
|
53
|
+
*/
|
|
54
|
+
async function uninstall(cwd = process.cwd(), options = {}) {
|
|
55
|
+
const readManifest = options.readManifest ?? defaultReadManifest;
|
|
56
|
+
const writeManifest = options.writeManifest ?? defaultWriteManifest;
|
|
57
|
+
const hashFile = options.hashFile ?? defaultHashFile;
|
|
58
|
+
const confirm = options.confirm ?? confirmUninstall;
|
|
59
|
+
const input = options.input ?? process.stdin;
|
|
60
|
+
const output = options.output ?? process.stdout;
|
|
61
|
+
|
|
62
|
+
const manifest = readManifest(cwd);
|
|
63
|
+
const entries = Object.entries(manifest.files);
|
|
64
|
+
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
console.info('Nada para desinstalar: Vetor não está instalado neste projeto.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.info(`${entries.length} arquivo(s) gerenciado(s) encontrado(s).`);
|
|
71
|
+
const confirmed = await confirm('Remover todos os arquivos instalados pelo Vetor?', { input, output });
|
|
72
|
+
if (!confirmed) {
|
|
73
|
+
console.info('Desinstalação cancelada.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const removed = [];
|
|
78
|
+
const preserved = [];
|
|
79
|
+
const remainingFiles = {};
|
|
80
|
+
|
|
81
|
+
for (const [key, entry] of entries) {
|
|
82
|
+
const destFile = path.resolve(cwd, ...key.split('/'));
|
|
83
|
+
const relative = path.relative(cwd, destFile);
|
|
84
|
+
const isInsideProject = relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
85
|
+
|
|
86
|
+
if (!isInsideProject) {
|
|
87
|
+
preserved.push({ path: key, reason: 'outside-project-root' });
|
|
88
|
+
remainingFiles[key] = entry;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!fs.existsSync(destFile)) {
|
|
93
|
+
preserved.push({ path: key, reason: 'already-missing' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (hashFile(destFile) !== entry.sha256) {
|
|
98
|
+
preserved.push({ path: key, reason: 'user-modified' });
|
|
99
|
+
remainingFiles[key] = entry;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.unlinkSync(destFile);
|
|
104
|
+
removed.push(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.info(`${removed.length} arquivo(s) removido(s).`);
|
|
108
|
+
if (preserved.length > 0) {
|
|
109
|
+
console.info(`${preserved.length} arquivo(s) preservado(s) (editado(s) pelo usuário ou já ausente(s)).`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Object.keys(remainingFiles).length > 0) {
|
|
113
|
+
writeManifest(cwd, { ...manifest, files: remainingFiles });
|
|
114
|
+
} else {
|
|
115
|
+
fs.rmSync(manifestPathFor(cwd), { force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { uninstall, confirmUninstall };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ENGINES } = require('../installer/detector.js');
|
|
4
|
+
const { installFiles: defaultInstallFiles } = require('../installer/writer.js');
|
|
5
|
+
const { readManifest: defaultReadManifest } = require('../installer/manifest.js');
|
|
6
|
+
|
|
7
|
+
const ENGINE_NAME_BY_ID = new Map(ENGINES.map((engine) => [engine.id, engine.name]));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Comando `update`: sincroniza a instalação existente sem repetir a detecção/seleção de
|
|
11
|
+
* engines do `install` (issue #301) — as engines a atualizar são derivadas do próprio
|
|
12
|
+
* manifesto (`.vetor/install-manifest.json`, campo `engine` de cada entrada), nunca de
|
|
13
|
+
* `detectEngines()`, para não arriscar instalar numa engine que o usuário nunca selecionou
|
|
14
|
+
* (ou deixar de atualizar uma que ele selecionou).
|
|
15
|
+
*
|
|
16
|
+
* A política de "update seguro" por arquivo (ausente copia, hash bate sincroniza,
|
|
17
|
+
* unmanaged/user-modified nunca sobrescreve) já vive em `installFiles`/`copyManagedFile` —
|
|
18
|
+
* este comando só monta os argumentos e reporta o resultado.
|
|
19
|
+
*
|
|
20
|
+
* Arquivo que constava no manifesto antes desta rodada e não aparece em `copied` nem
|
|
21
|
+
* `skipped` depois é órfão (fonte removeu o arquivo, ex.: skill descontinuada): reportado,
|
|
22
|
+
* nunca apagado — deleção fica reservada ao comando `uninstall`.
|
|
23
|
+
*
|
|
24
|
+
* `readManifest`/`installFiles` são injetáveis para testes.
|
|
25
|
+
*/
|
|
26
|
+
function update(cwd = process.cwd(), options = {}) {
|
|
27
|
+
const readManifest = options.readManifest ?? defaultReadManifest;
|
|
28
|
+
const installFiles = options.installFiles ?? defaultInstallFiles;
|
|
29
|
+
|
|
30
|
+
const manifestBefore = readManifest(cwd);
|
|
31
|
+
const keysBefore = Object.keys(manifestBefore.files);
|
|
32
|
+
|
|
33
|
+
if (keysBefore.length === 0) {
|
|
34
|
+
console.info('Nenhuma instalação encontrada neste projeto. Rode "vetor install" primeiro.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const engineIds = [...new Set(Object.values(manifestBefore.files).map((entry) => entry.engine))];
|
|
39
|
+
const engines = engineIds.map((id) => ({ id, name: ENGINE_NAME_BY_ID.get(id) ?? id }));
|
|
40
|
+
|
|
41
|
+
console.info(`Atualizando engines: ${engines.map((engine) => engine.name).join(', ')}.`);
|
|
42
|
+
|
|
43
|
+
const { copied, skipped, warnings = [] } = installFiles({ projectRoot: cwd, engines });
|
|
44
|
+
|
|
45
|
+
console.info(`${copied.length} arquivo(s) sincronizado(s).`);
|
|
46
|
+
if (skipped.length > 0) {
|
|
47
|
+
console.info(
|
|
48
|
+
`${skipped.length} arquivo(s) não sobrescrito(s) (editado(s) pelo usuário ou não gerado(s) pelo instalador).`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
for (const warning of warnings) {
|
|
52
|
+
console.info(`Aviso: ${warning}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const touched = new Set([...copied, ...skipped.map((entry) => entry.path)]);
|
|
56
|
+
for (const key of keysBefore) {
|
|
57
|
+
if (!touched.has(key)) {
|
|
58
|
+
console.info(`${key}: não existe mais na fonte (não removido — use "vetor uninstall" se quiser limpar).`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { update };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifica se um comando existe em algum diretório do PATH, sem executá-lo — apenas
|
|
8
|
+
* resolve candidatos no filesystem (evita spawn/child_process e efeitos colaterais).
|
|
9
|
+
* No Windows, cruza cada diretório do PATH com as extensões de PATHEXT (`.cmd`, `.exe`, ...),
|
|
10
|
+
* já que CLIs instaladas via npm (`claude`, `codex`, `opencode`) viram shims `.cmd`.
|
|
11
|
+
*
|
|
12
|
+
* `env` é injetável para testes determinísticos (ex.: PATH vazio → nenhum comando encontrado).
|
|
13
|
+
*/
|
|
14
|
+
function commandExists(command, env = process.env) {
|
|
15
|
+
const pathEnv = env.PATH ?? env.Path ?? '';
|
|
16
|
+
const dirs = pathEnv.split(path.delimiter).filter(Boolean);
|
|
17
|
+
const extensions = process.platform === 'win32'
|
|
18
|
+
? (env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';')
|
|
19
|
+
: [''];
|
|
20
|
+
|
|
21
|
+
for (const dir of dirs) {
|
|
22
|
+
for (const ext of extensions) {
|
|
23
|
+
const candidate = path.join(dir, command + ext);
|
|
24
|
+
if (fs.existsSync(candidate)) return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { commandExists };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tradução de hooks/hooks.json (formato Claude Code, fonte agnóstica deste repositório) para
|
|
4
|
+
// o schema nativo do Cursor (`.cursor/hooks.json`), confirmado contra a documentação oficial:
|
|
5
|
+
// - cursor.com/docs/hooks (schema, `version`, `hooks.<evento>[]` com `command`/`matcher`/
|
|
6
|
+
// `timeout`, lista de eventos suportados)
|
|
7
|
+
// - cursor.com/docs/reference/third-party-hooks (mapeamento de nomes de evento e de tool entre
|
|
8
|
+
// Claude Code e Cursor)
|
|
9
|
+
// Ver wiki/Compatibilidade-Cursor.md, seção Hooks, para o achado completo (issue #284).
|
|
10
|
+
//
|
|
11
|
+
// Cobre só os eventos usados por hooks/hooks.json deste repositório hoje (YAGNI) — não é um
|
|
12
|
+
// tradutor genérico para qualquer hooks.json arbitrário de terceiros.
|
|
13
|
+
|
|
14
|
+
// cursor.com/docs/reference/third-party-hooks#hook-step-mapping — só os eventos que essa
|
|
15
|
+
// tabela documenta como suportados; eventos sem linha na tabela (ex.: WorktreeCreate, que nem
|
|
16
|
+
// existe no Claude Code como conceito de hook nativo do produto, só neste plugin) não têm
|
|
17
|
+
// equivalente e são reportados em `dropped`.
|
|
18
|
+
const EVENT_NAME_MAP = {
|
|
19
|
+
PreToolUse: 'preToolUse',
|
|
20
|
+
PostToolUse: 'postToolUse',
|
|
21
|
+
UserPromptSubmit: 'beforeSubmitPrompt',
|
|
22
|
+
Stop: 'stop',
|
|
23
|
+
SubagentStop: 'subagentStop',
|
|
24
|
+
SessionStart: 'sessionStart',
|
|
25
|
+
SessionEnd: 'sessionEnd',
|
|
26
|
+
PreCompact: 'preCompact',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// cursor.com/docs/reference/third-party-hooks#tool-name-mapping. Edit e Write mapeiam para o
|
|
30
|
+
// mesmo tool Cursor ("Write") — o Cursor não distingui edição em lugar de escrita nova.
|
|
31
|
+
const TOOL_NAME_MAP = {
|
|
32
|
+
Bash: 'Shell',
|
|
33
|
+
Read: 'Read',
|
|
34
|
+
Write: 'Write',
|
|
35
|
+
Edit: 'Write',
|
|
36
|
+
Grep: 'Grep',
|
|
37
|
+
Task: 'Task',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Eventos cujo `matcher` do Claude Code filtra por NOME de tool — únicos onde o vocabulário de
|
|
41
|
+
// TOOL_NAME_MAP se aplica. Outros eventos com matcher (ex.: SubagentStop, que no Claude Code
|
|
42
|
+
// filtra por nome customizado de subagente) têm semântica diferente da do Cursor (que filtra
|
|
43
|
+
// por `subagent_type` fixo: generalPurpose/explore/shell) e não são traduzíveis — ver
|
|
44
|
+
// `translateHooksForCursor`.
|
|
45
|
+
const TOOL_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse']);
|
|
46
|
+
|
|
47
|
+
// Lista completa de eventos documentados em cursor.com/docs/hooks (Agent hooks + Tab hooks +
|
|
48
|
+
// App lifecycle hook) — usada por `validateCursorHooksSchema` para rejeitar nomes de evento
|
|
49
|
+
// inventados/não suportados, não só os que este tradutor emite.
|
|
50
|
+
const CURSOR_EVENT_NAMES = new Set([
|
|
51
|
+
'sessionStart',
|
|
52
|
+
'sessionEnd',
|
|
53
|
+
'preToolUse',
|
|
54
|
+
'postToolUse',
|
|
55
|
+
'postToolUseFailure',
|
|
56
|
+
'subagentStart',
|
|
57
|
+
'subagentStop',
|
|
58
|
+
'beforeShellExecution',
|
|
59
|
+
'afterShellExecution',
|
|
60
|
+
'beforeMCPExecution',
|
|
61
|
+
'afterMCPExecution',
|
|
62
|
+
'beforeReadFile',
|
|
63
|
+
'afterFileEdit',
|
|
64
|
+
'beforeSubmitPrompt',
|
|
65
|
+
'preCompact',
|
|
66
|
+
'stop',
|
|
67
|
+
'afterAgentResponse',
|
|
68
|
+
'afterAgentThought',
|
|
69
|
+
'beforeTabFileRead',
|
|
70
|
+
'afterTabFileEdit',
|
|
71
|
+
'workspaceOpen',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Traduz o `matcher` de PreToolUse/PostToolUse (nomes de tool do Claude Code, string "|"-
|
|
76
|
+
* separated) para o vocabulário de tool do Cursor, removendo duplicatas geradas por tools que
|
|
77
|
+
* colapsam no mesmo tool Cursor (Edit/Write -> Write). Tokens fora de TOOL_NAME_MAP passam
|
|
78
|
+
* inalterados (ex.: um nome de tool MCP customizado).
|
|
79
|
+
*/
|
|
80
|
+
function translateToolMatcher(matcher) {
|
|
81
|
+
const tokens = matcher.split('|').map((token) => TOOL_NAME_MAP[token] ?? token);
|
|
82
|
+
return [...new Set(tokens)].join('|');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Achata uma entrada `{matcher?, hooks:[{type,command,timeout}]}` do Claude Code em uma ou mais entradas planas `{command,timeout?,matcher?}` do Cursor. */
|
|
86
|
+
function translateHookEntry(event, entry, canTranslateMatcher) {
|
|
87
|
+
const matcher = entry.matcher;
|
|
88
|
+
return (entry.hooks ?? []).map((inner) => {
|
|
89
|
+
const translated = { command: inner.command };
|
|
90
|
+
if (typeof inner.timeout === 'number') translated.timeout = inner.timeout;
|
|
91
|
+
if (matcher && canTranslateMatcher) {
|
|
92
|
+
translated.matcher = translateToolMatcher(matcher);
|
|
93
|
+
}
|
|
94
|
+
return translated;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Traduz `hooks/hooks.json` (formato Claude Code) para o schema nativo do Cursor
|
|
100
|
+
* (`.cursor/hooks.json`). Retorna `{ hooks, dropped }`:
|
|
101
|
+
* - `hooks`: objeto pronto para `JSON.stringify` em `.cursor/hooks.json` (com `version: 1`).
|
|
102
|
+
* - `dropped`: eventos/matchers da fonte sem equivalente fiel no Cursor — nunca lançado como
|
|
103
|
+
* erro (perda de cobertura parcial não deve travar a instalação), mas sempre reportado para
|
|
104
|
+
* quem for decidir o que fazer com o gap (ver wiki/Compatibilidade-Cursor.md).
|
|
105
|
+
*/
|
|
106
|
+
function translateHooksForCursor(sourceHooksJson) {
|
|
107
|
+
const sourceEvents = sourceHooksJson?.hooks ?? {};
|
|
108
|
+
const translatedHooks = {};
|
|
109
|
+
const dropped = [];
|
|
110
|
+
|
|
111
|
+
for (const [event, entries] of Object.entries(sourceEvents)) {
|
|
112
|
+
const cursorEvent = EVENT_NAME_MAP[event];
|
|
113
|
+
if (!cursorEvent) {
|
|
114
|
+
dropped.push({ event, reason: 'no-cursor-equivalent' });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const canTranslateMatcher = TOOL_MATCHER_EVENTS.has(event);
|
|
119
|
+
const hasUntranslatableMatcher = entries.some(
|
|
120
|
+
(entry) => entry.matcher && !canTranslateMatcher,
|
|
121
|
+
);
|
|
122
|
+
if (hasUntranslatableMatcher) {
|
|
123
|
+
dropped.push({ event, reason: 'matcher-not-translatable' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
translatedHooks[cursorEvent] = entries.flatMap((entry) =>
|
|
127
|
+
translateHookEntry(event, entry, canTranslateMatcher),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { hooks: { version: 1, hooks: translatedHooks }, dropped };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validação mínima do `.cursor/hooks.json` gerado contra o schema documentado em
|
|
136
|
+
* cursor.com/docs/hooks (nomes de evento válidos, `version` numérica, `command` obrigatório
|
|
137
|
+
* por entrada). Não é um validador de JSON Schema genérico — cobre só os campos que este
|
|
138
|
+
* tradutor emite e os erros mais prováveis de regressão (evento inventado, entrada sem
|
|
139
|
+
* `command`), suficiente para o critério de aceite da issue #284 sem depender de uma lib
|
|
140
|
+
* externa (este pacote não tem dependências — ver cli/package.json).
|
|
141
|
+
*/
|
|
142
|
+
function validateCursorHooksSchema(hooksJson) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
|
|
145
|
+
if (typeof hooksJson?.version !== 'number') {
|
|
146
|
+
errors.push('"version" ausente ou não-numérico — cursor.com/docs/hooks exige um inteiro positivo (ex.: 1)');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hooks = hooksJson?.hooks;
|
|
150
|
+
if (hooks && typeof hooks === 'object') {
|
|
151
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
152
|
+
if (!CURSOR_EVENT_NAMES.has(event)) {
|
|
153
|
+
errors.push(`evento "${event}" não está na lista documentada em cursor.com/docs/hooks`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const [index, entry] of (entries ?? []).entries()) {
|
|
157
|
+
if (typeof entry?.command !== 'string' || entry.command.length === 0) {
|
|
158
|
+
errors.push(`hooks.${event}[${index}] sem "command" (campo obrigatório)`);
|
|
159
|
+
}
|
|
160
|
+
if ('timeout' in entry && typeof entry.timeout !== 'number') {
|
|
161
|
+
errors.push(`hooks.${event}[${index}].timeout precisa ser numérico`);
|
|
162
|
+
}
|
|
163
|
+
if ('matcher' in entry && typeof entry.matcher !== 'string') {
|
|
164
|
+
errors.push(`hooks.${event}[${index}].matcher precisa ser string`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
errors.push('"hooks" ausente ou não é um objeto');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { valid: errors.length === 0, errors };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
translateHooksForCursor,
|
|
177
|
+
validateCursorHooksSchema,
|
|
178
|
+
EVENT_NAME_MAP,
|
|
179
|
+
TOOL_NAME_MAP,
|
|
180
|
+
CURSOR_EVENT_NAMES,
|
|
181
|
+
};
|