@promovaweb/brandfy 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog do pacote npm
2
+
3
+ O changelog canônico da release fica em
4
+ [`../CHANGELOG.md`](../CHANGELOG.md), pois o CLI, o framework e a documentação
5
+ usam a mesma SemVer.
6
+
7
+ ## [1.1.0] - 2026-07-30
8
+
9
+ - Primeira publicação do comando `brandfy`.
10
+ - Instalação e atualização das skills por meio do pacote `skills`.
11
+ - Comandos de setup, diagnóstico, PDF e auditoria.
12
+
13
+ [1.1.0]: https://github.com/promovaweb/brandfy/releases/tag/v1.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Promovaweb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # CLI do Brandfy
2
+
3
+ O pacote `@promovaweb/brandfy` instala as skills oficiais e oferece atalhos
4
+ para preparar, conferir e compilar a marca de um projeto. O executável usa o
5
+ pacote `skills` como dependência fixa, portanto a instalação e a atualização
6
+ continuam compatíveis com `skills-lock.json`.
7
+
8
+ ## Instalação
9
+
10
+ Instale o executável globalmente:
11
+
12
+ ```bash
13
+ npm install --global @promovaweb/brandfy
14
+ brandfy init .
15
+ ```
16
+
17
+ Também é possível executar sem instalação global:
18
+
19
+ ```bash
20
+ npx @promovaweb/brandfy init .
21
+ ```
22
+
23
+ O comando `init` instala todas as skills no projeto, prepara `.brandfy/`,
24
+ cria a estrutura inicial em `brand/` e inclui o bloco gerenciado no
25
+ `AGENTS.md`.
26
+
27
+ ## Comandos
28
+
29
+ | Comando | Resultado |
30
+ | --- | --- |
31
+ | `brandfy init [diretório]` | Instala as skills e executa o setup. |
32
+ | `brandfy doctor [diretório]` | Confere Node.js, skills, lock, configuração, manual e instruções. |
33
+ | `brandfy skills list` | Lista o catálogo disponível na origem configurada. |
34
+ | `brandfy skills install [diretório]` | Instala todas as skills pelo `skills add`. |
35
+ | `brandfy skills update [diretório]` | Reinstala somente as skills do Brandfy a partir da origem. |
36
+ | `brandfy pdf [diretório]` | Compila `brand/README.md` em `brand/brand-guide.pdf`. |
37
+ | `brandfy audit [diretório]` | Executa a auditoria e atualiza `.brandfy/audit.md`. |
38
+ | `brandfy update [diretório]` | Atualiza as skills e executa o setup em modo de conferência. |
39
+
40
+ Use `--source <origem>` para testar um checkout local ou outra origem aceita
41
+ pelo gerenciador. O destino padrão é `promovaweb/brandfy`, e o agente padrão é
42
+ o Codex.
43
+
44
+ Os argumentos escritos depois de `--` seguem para o script da skill. Este
45
+ exemplo escolhe outro nome para o PDF:
46
+
47
+ ```bash
48
+ brandfy pdf . -- --output brand/manual-da-marca.pdf
49
+ ```
50
+
51
+ ## Atualizações
52
+
53
+ O framework e o CLI compartilham a mesma SemVer. Atualize o executável pelo npm
54
+ e, dentro do projeto, atualize as skills:
55
+
56
+ ```bash
57
+ npm install --global @promovaweb/brandfy@latest
58
+ brandfy update .
59
+ brandfy doctor .
60
+ ```
61
+
62
+ O comando `brandfy update` não substitui arquivos da marca que pertencem ao
63
+ usuário. O setup executado na sequência confere a estrutura e informa qualquer
64
+ arquivo ausente.
65
+
66
+ ## Desenvolvimento
67
+
68
+ Na raiz do repositório Brandfy, instale as dependências e execute os testes:
69
+
70
+ ```bash
71
+ npm --prefix cli ci
72
+ npm run cli:test
73
+ npm run release:check
74
+ ```
75
+
76
+ Consulte [`RELEASING.md`](../RELEASING.md) para atualizar a versão, gerar a
77
+ documentação, publicar no npm e criar a tag e a GitHub Release.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Expõe o executável público do Brandfy.
5
+ *
6
+ * O arquivo permanece pequeno para que toda a lógica possa ser importada e
7
+ * exercitada pela suíte sem iniciar outro processo.
8
+ */
9
+
10
+ import { main } from "../src/cli.mjs";
11
+
12
+ await main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@promovaweb/brandfy",
3
+ "version": "1.1.0",
4
+ "description": "CLI do Brandfy para instalar skills e manter sistemas de marca em projetos com agentes.",
5
+ "type": "module",
6
+ "bin": {
7
+ "brandfy": "bin/brandfy.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "CHANGELOG.md",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "node --test tests/*.test.mjs",
18
+ "prepack": "npm test"
19
+ },
20
+ "engines": {
21
+ "node": ">=22.20.0"
22
+ },
23
+ "keywords": [
24
+ "agents",
25
+ "branding",
26
+ "brand",
27
+ "cli",
28
+ "skills"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/promovaweb/brandfy.git",
34
+ "directory": "cli"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/promovaweb/brandfy/issues"
38
+ },
39
+ "homepage": "https://github.com/promovaweb/brandfy#readme",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "skills": "1.5.21"
45
+ }
46
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Implementa a interface de linha de comando do Brandfy.
3
+ *
4
+ * O CLI delega instalação e atualização ao pacote `skills`. Os comandos de
5
+ * geração executam os scripts das skills instaladas no projeto consumidor.
6
+ */
7
+
8
+ import path from "node:path";
9
+ import { readFile } from "node:fs/promises";
10
+ import { fileURLToPath } from "node:url";
11
+ import { createProcessRunner } from "./process-runner.mjs";
12
+ import {
13
+ inspectProject,
14
+ locateSkillScript,
15
+ } from "./project.mjs";
16
+
17
+ const packageRoot = path.resolve(
18
+ path.dirname(fileURLToPath(import.meta.url)),
19
+ "..",
20
+ );
21
+ const packageManifest = JSON.parse(
22
+ await readFile(path.join(packageRoot, "package.json"), "utf8"),
23
+ );
24
+
25
+ export const DEFAULT_SOURCE = "promovaweb/brandfy";
26
+ export const DEFAULT_AGENT = "codex";
27
+
28
+ const HELP = `Brandfy ${packageManifest.version}
29
+
30
+ Instala as skills do Brandfy e executa os geradores no projeto atual.
31
+
32
+ Uso:
33
+ brandfy init [diretório] Instala as skills e prepara a marca
34
+ brandfy doctor [diretório] [--json] Confere a instalação e os arquivos
35
+ brandfy skills list Lista as skills disponíveis
36
+ brandfy skills install [diretório] Instala todas as skills
37
+ brandfy skills update [diretório] Atualiza somente as skills do Brandfy
38
+ brandfy pdf [diretório] [-- ...] Compila brand/README.md em PDF
39
+ brandfy audit [diretório] [-- ...] Audita a estrutura da marca
40
+ brandfy update [diretório] Atualiza as skills e confere o setup
41
+ brandfy --version Mostra a versão do CLI
42
+
43
+ Opções:
44
+ --source <origem> Repositório ou caminho aceito pelo skills add
45
+ --agent <agente> Agente de destino (padrão: codex)
46
+ --json Emite o diagnóstico como JSON
47
+ --help Mostra esta ajuda
48
+
49
+ Os argumentos escritos depois de -- seguem para o gerador de PDF ou auditoria.
50
+ `;
51
+
52
+ function parseArguments(argv) {
53
+ const positional = [];
54
+ const passthrough = [];
55
+ const options = {
56
+ source: DEFAULT_SOURCE,
57
+ agent: DEFAULT_AGENT,
58
+ json: false,
59
+ };
60
+ let forwarding = false;
61
+
62
+ for (let index = 0; index < argv.length; index += 1) {
63
+ const argument = argv[index];
64
+ if (forwarding) {
65
+ passthrough.push(argument);
66
+ } else if (argument === "--") {
67
+ forwarding = true;
68
+ } else if (argument === "--source" || argument === "--agent") {
69
+ const value = argv[index + 1];
70
+ if (!value) throw new Error(`${argument} exige um valor.`);
71
+ options[argument.slice(2)] = value;
72
+ index += 1;
73
+ } else if (argument === "--json") {
74
+ options.json = true;
75
+ } else if (argument.startsWith("-")) {
76
+ throw new Error(`Opção desconhecida: ${argument}`);
77
+ } else {
78
+ positional.push(argument);
79
+ }
80
+ }
81
+
82
+ return { positional, passthrough, options };
83
+ }
84
+
85
+ function projectFrom(value, cwd) {
86
+ return path.resolve(cwd, value || ".");
87
+ }
88
+
89
+ async function ensureSuccess(result, action) {
90
+ if (result.code !== 0) {
91
+ const detail = result.stderr?.trim() || result.stdout?.trim();
92
+ throw new Error(detail ? `${action}: ${detail}` : `${action}.`);
93
+ }
94
+ }
95
+
96
+ async function installSkills(runner, project, options, label) {
97
+ console.log(`${label} as skills do Brandfy em ${project}...`);
98
+ const result = await runner.runSkills([
99
+ "add",
100
+ options.source,
101
+ "--agent",
102
+ options.agent,
103
+ "--skill",
104
+ "*",
105
+ "-y",
106
+ "--copy",
107
+ ], project);
108
+ await ensureSuccess(result, "O gerenciador skills não concluiu a instalação");
109
+ }
110
+
111
+ async function runInstalledScript(
112
+ runner,
113
+ project,
114
+ skill,
115
+ relativeScript,
116
+ args,
117
+ ) {
118
+ const script = await locateSkillScript(project, skill, relativeScript);
119
+ if (!script) {
120
+ throw new Error(
121
+ `A skill ${skill} não está instalada. Execute brandfy init primeiro.`,
122
+ );
123
+ }
124
+ const result = await runner.runScript(script, args, project);
125
+ await ensureSuccess(result, `O script da skill ${skill} falhou`);
126
+ }
127
+
128
+ /**
129
+ * Executa um comando com dependências injetáveis para permitir testes isolados.
130
+ *
131
+ * @param {string[]} argv Argumentos sem `node` e sem o nome do binário.
132
+ * @param {{cwd?: string, runner?: object, stdout?: Function}} dependencies
133
+ * Dependências substituíveis pela suíte.
134
+ * @returns {Promise<number>} Código de saída.
135
+ */
136
+ export async function execute(argv, dependencies = {}) {
137
+ const cwd = dependencies.cwd || process.cwd();
138
+ const runner = dependencies.runner || createProcessRunner();
139
+ const stdout = dependencies.stdout || console.log;
140
+
141
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
142
+ stdout(HELP.trimEnd());
143
+ return 0;
144
+ }
145
+ if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
146
+ stdout(packageManifest.version);
147
+ return 0;
148
+ }
149
+
150
+ const { positional, passthrough, options } = parseArguments(argv);
151
+ const [command, subcommand, possibleProject] = positional;
152
+
153
+ if (command === "skills" && subcommand === "list") {
154
+ const result = await runner.runSkills(
155
+ ["add", options.source, "--list"],
156
+ cwd,
157
+ );
158
+ await ensureSuccess(result, "Não foi possível listar as skills");
159
+ return 0;
160
+ }
161
+
162
+ if (
163
+ command === "skills"
164
+ && (subcommand === "install" || subcommand === "update")
165
+ ) {
166
+ const project = projectFrom(possibleProject, cwd);
167
+ await installSkills(
168
+ runner,
169
+ project,
170
+ options,
171
+ subcommand === "install" ? "Instalando" : "Atualizando",
172
+ );
173
+ return 0;
174
+ }
175
+
176
+ const project = projectFrom(subcommand, cwd);
177
+ if (command === "init") {
178
+ await installSkills(runner, project, options, "Instalando");
179
+ await runInstalledScript(
180
+ runner,
181
+ project,
182
+ "brandfy-setup",
183
+ "scripts/setup.mjs",
184
+ ["--project", project],
185
+ );
186
+ stdout(`Brandfy preparado em ${project}.`);
187
+ return 0;
188
+ }
189
+
190
+ if (command === "doctor") {
191
+ const checks = await inspectProject(project);
192
+ if (options.json) {
193
+ stdout(JSON.stringify({ project, checks }, null, 2));
194
+ } else {
195
+ stdout(`Diagnóstico do Brandfy em ${project}`);
196
+ for (const check of checks) {
197
+ stdout(`${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
198
+ }
199
+ }
200
+ return checks.every((check) => check.ok) ? 0 : 1;
201
+ }
202
+
203
+ if (command === "pdf") {
204
+ await runInstalledScript(
205
+ runner,
206
+ project,
207
+ "brandfy-guia-pdf",
208
+ "scripts/build-brand-guide.mjs",
209
+ ["--project", project, ...passthrough],
210
+ );
211
+ return 0;
212
+ }
213
+
214
+ if (command === "audit") {
215
+ await runInstalledScript(
216
+ runner,
217
+ project,
218
+ "brandfy-auditoria",
219
+ "scripts/audit-brand.mjs",
220
+ ["--project", project, ...passthrough],
221
+ );
222
+ return 0;
223
+ }
224
+
225
+ if (command === "update") {
226
+ await installSkills(runner, project, options, "Atualizando");
227
+ await runInstalledScript(
228
+ runner,
229
+ project,
230
+ "brandfy-setup",
231
+ "scripts/setup.mjs",
232
+ ["--project", project, "--check"],
233
+ );
234
+ stdout(`Skills atualizadas e setup conferido em ${project}.`);
235
+ return 0;
236
+ }
237
+
238
+ throw new Error(`Comando desconhecido: ${command || "(vazio)"}`);
239
+ }
240
+
241
+ /**
242
+ * Converte falhas esperadas em uma mensagem curta de terminal.
243
+ *
244
+ * @param {string[]} argv Argumentos do processo.
245
+ * @returns {Promise<void>}
246
+ */
247
+ export async function main(argv) {
248
+ try {
249
+ process.exitCode = await execute(argv);
250
+ } catch (error) {
251
+ console.error(`Erro: ${error.message}`);
252
+ process.exitCode = 1;
253
+ }
254
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Executa o gerenciador `skills` e os scripts instalados pelo Brandfy.
3
+ *
4
+ * A resolução parte da dependência empacotada, portanto o CLI não depende de
5
+ * um comando `skills` global nem baixa uma versão diferente a cada execução.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { readFile } from "node:fs/promises";
10
+ import { createRequire } from "node:module";
11
+ import path from "node:path";
12
+
13
+ const require = createRequire(import.meta.url);
14
+
15
+ /**
16
+ * Executa um processo filho com o terminal herdado.
17
+ *
18
+ * @param {string} command Executável que será iniciado.
19
+ * @param {string[]} args Argumentos entregues ao executável.
20
+ * @param {string} cwd Diretório usado durante a execução.
21
+ * @param {"inherit" | "pipe"} stdio Forma de encaminhar a saída.
22
+ * @returns {Promise<{code: number, stdout: string, stderr: string}>}
23
+ */
24
+ export function runProcess(command, args, cwd, stdio = "inherit") {
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawn(command, args, {
27
+ cwd,
28
+ stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
29
+ env: process.env,
30
+ });
31
+ let stdout = "";
32
+ let stderr = "";
33
+
34
+ if (stdio === "pipe") {
35
+ child.stdout.setEncoding("utf8");
36
+ child.stderr.setEncoding("utf8");
37
+ child.stdout.on("data", (chunk) => {
38
+ stdout += chunk;
39
+ });
40
+ child.stderr.on("data", (chunk) => {
41
+ stderr += chunk;
42
+ });
43
+ }
44
+
45
+ child.once("error", reject);
46
+ child.once("close", (code) => {
47
+ resolve({ code: code ?? 1, stdout, stderr });
48
+ });
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Localiza o binário distribuído pela dependência npm `skills`.
54
+ *
55
+ * @returns {Promise<string>} Caminho absoluto do arquivo executável.
56
+ */
57
+ export async function resolveSkillsCli() {
58
+ const packagePath = require.resolve("skills/package.json");
59
+ const manifest = JSON.parse(await readFile(packagePath, "utf8"));
60
+ const relativeBin = typeof manifest.bin === "string"
61
+ ? manifest.bin
62
+ : manifest.bin?.skills;
63
+
64
+ if (!relativeBin) {
65
+ throw new Error("A dependência skills não expõe o executável esperado.");
66
+ }
67
+
68
+ return path.resolve(path.dirname(packagePath), relativeBin);
69
+ }
70
+
71
+ /**
72
+ * Cria o adaptador usado pelos comandos do Brandfy.
73
+ *
74
+ * @returns {{runSkills: Function, runScript: Function}} Adaptador de processos.
75
+ */
76
+ export function createProcessRunner() {
77
+ return {
78
+ async runSkills(args, cwd, stdio = "inherit") {
79
+ const executable = await resolveSkillsCli();
80
+ return runProcess(process.execPath, [executable, ...args], cwd, stdio);
81
+ },
82
+ runScript(script, args, cwd, stdio = "inherit") {
83
+ return runProcess(process.execPath, [script, ...args], cwd, stdio);
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Resolve a instalação do Brandfy e confere o estado do projeto consumidor.
3
+ *
4
+ * A busca aceita os diretórios usados pelos agentes mais comuns, mas o CLI
5
+ * instala no escopo do projeto para manter o lock junto do repositório.
6
+ */
7
+
8
+ import { access, readFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+
11
+ export const SKILL_DIRECTORIES = [
12
+ ".agents/skills",
13
+ ".codex/skills",
14
+ ".claude/skills",
15
+ ];
16
+
17
+ export async function exists(target) {
18
+ try {
19
+ await access(target);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Localiza um script pertencente a uma skill instalada.
28
+ *
29
+ * @param {string} project Diretório absoluto do projeto consumidor.
30
+ * @param {string} skill Nome da skill.
31
+ * @param {string} script Caminho relativo dentro da skill.
32
+ * @returns {Promise<string | null>} Caminho encontrado ou `null`.
33
+ */
34
+ export async function locateSkillScript(project, skill, script) {
35
+ for (const directory of SKILL_DIRECTORIES) {
36
+ const candidate = path.join(project, directory, skill, script);
37
+ if (await exists(candidate)) return candidate;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function supportsRequiredNode(version) {
43
+ const [major, minor] = version.split(".").map(Number);
44
+ return major > 22 || (major === 22 && minor >= 20);
45
+ }
46
+
47
+ /**
48
+ * Produz as verificações exibidas por `brandfy doctor`.
49
+ *
50
+ * @param {string} project Diretório absoluto do projeto.
51
+ * @returns {Promise<Array<{name: string, ok: boolean, detail: string}>>}
52
+ */
53
+ export async function inspectProject(project) {
54
+ const setup = await locateSkillScript(
55
+ project,
56
+ "brandfy-setup",
57
+ "scripts/setup.mjs",
58
+ );
59
+ const agentsPath = path.join(project, "AGENTS.md");
60
+ const agents = await exists(agentsPath)
61
+ ? await readFile(agentsPath, "utf8")
62
+ : "";
63
+
64
+ return [
65
+ {
66
+ name: "Node.js",
67
+ ok: supportsRequiredNode(process.versions.node),
68
+ detail: `versão ${process.versions.node}`,
69
+ },
70
+ {
71
+ name: "Skills do Brandfy",
72
+ ok: Boolean(setup),
73
+ detail: setup
74
+ ? path.relative(project, setup)
75
+ : "brandfy-setup não foi encontrada",
76
+ },
77
+ {
78
+ name: "Lock das skills",
79
+ ok: await exists(path.join(project, "skills-lock.json")),
80
+ detail: "skills-lock.json",
81
+ },
82
+ {
83
+ name: "Configuração",
84
+ ok: await exists(path.join(project, ".brandfy", "config.yaml")),
85
+ detail: ".brandfy/config.yaml",
86
+ },
87
+ {
88
+ name: "Manual editável",
89
+ ok: await exists(path.join(project, "brand", "README.md")),
90
+ detail: "brand/README.md",
91
+ },
92
+ {
93
+ name: "Instruções do agente",
94
+ ok: agents.includes("<!-- brandfy:consumer:start -->")
95
+ && agents.includes("<!-- brandfy:consumer:end -->"),
96
+ detail: "bloco gerenciado no AGENTS.md",
97
+ },
98
+ ];
99
+ }