@bacano/sdk 1.1.9 → 1.1.12

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.
@@ -0,0 +1,39 @@
1
+ # Validación visual y funcional
2
+
3
+ Validar por cortes para evitar corregir 70 secciones al final.
4
+
5
+ ## Evidencia mínima por página
6
+
7
+ - URL/ruta implementada.
8
+ - Frame y node ID de referencia.
9
+ - Viewport desktop y mobile utilizados.
10
+ - Captura de referencia y captura de implementación.
11
+ - Diferencias intencionales e inferencias.
12
+ - Resultado de accesibilidad/interacción aplicable.
13
+
14
+ ## Orden de revisión
15
+
16
+ 1. Estructura, jerarquía y contenido.
17
+ 2. Breakpoints, anchuras y flujo responsive.
18
+ 3. Tipografía, color, espacios y radios.
19
+ 4. Imágenes, recortes, densidad y estado de carga.
20
+ 5. Hover, focus, teclado, formularios y navegación.
21
+ 6. Estados loading, vacío, error y contenido largo.
22
+ 7. Integración Bacano y ausencia de datos inventados.
23
+ 8. Contenido editorial real o fixture temporal claramente identificado.
24
+
25
+ Usar Playwright o la herramienta de navegador disponible para capturas y
26
+ traces. Una comparación pixel a pixel puede ayudar, pero no debe ocultar fallos
27
+ semánticos o funcionales.
28
+
29
+ ## Cierre técnico
30
+
31
+ - Typecheck, lint, tests y build del proyecto.
32
+ - Rutas generadas y sitemap esperado.
33
+ - Sin errores de consola/recurso.
34
+ - Sin GraphQL directo ni solicitudes duplicadas evitables.
35
+ - HTML estático presente cuando el contrato es Static First.
36
+ - Media responsive sin upscale visible.
37
+ - Ningún fixture, placeholder o fallback editorial temporal en el build de
38
+ producción.
39
+ - `$bacano-web` ejecutado al final cuando corresponda.
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import {
5
+ cpSync,
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ readlinkSync,
12
+ rmSync,
13
+ symlinkSync,
14
+ } from "node:fs";
15
+ import { connect } from "node:net";
16
+ import { dirname, relative, resolve } from "node:path";
17
+ import { createInterface } from "node:readline/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { validateSnapshot } from "./snapshot.mjs";
20
+
21
+ const skillName = "bacano-figma-web";
22
+ const scriptDirectory = dirname(fileURLToPath(import.meta.url));
23
+ const sourceSkill = resolve(scriptDirectory, "..");
24
+ const projectRoot = process.cwd();
25
+ const canonicalSkill = resolve(projectRoot, ".agents", "skills", skillName);
26
+
27
+ const adapterPaths = {
28
+ antigravity: canonicalSkill,
29
+ claude: resolve(projectRoot, ".claude", "skills", skillName),
30
+ codex: resolve(projectRoot, ".codex", "skills", skillName),
31
+ };
32
+
33
+ function usage() {
34
+ console.log(`Bacano Figma Web
35
+
36
+ Uso:
37
+ bacano-figma-web install [--agent antigravity|claude|codex|all] [--force]
38
+ bacano-figma-web check
39
+ bacano-figma-web doctor [--json]
40
+ bacano-figma-web validate-snapshot <ruta/manifest.json>
41
+
42
+ La copia canónica vive en .agents/skills/${skillName}.
43
+ Claude y Codex reciben enlaces a esa misma copia.`);
44
+ }
45
+
46
+ function option(name) {
47
+ const index = process.argv.indexOf(name);
48
+ return index >= 0 ? process.argv[index + 1] : null;
49
+ }
50
+
51
+ function relativeDisplay(path) {
52
+ const value = relative(projectRoot, path);
53
+ return value || ".";
54
+ }
55
+
56
+ function listFiles(directory, base = directory) {
57
+ return readdirSync(directory, { withFileTypes: true })
58
+ .flatMap((entry) => {
59
+ const path = resolve(directory, entry.name);
60
+ if (entry.isDirectory()) return listFiles(path, base);
61
+ if (!entry.isFile()) return [];
62
+ return [{ path, name: relative(base, path) }];
63
+ })
64
+ .sort((left, right) => left.name.localeCompare(right.name));
65
+ }
66
+
67
+ function skillDigest(directory) {
68
+ const hash = createHash("sha256");
69
+ for (const file of listFiles(directory)) {
70
+ hash.update(file.name);
71
+ hash.update("\0");
72
+ hash.update(readFileSync(file.path));
73
+ hash.update("\0");
74
+ }
75
+ return hash.digest("hex");
76
+ }
77
+
78
+ function isSkillDirectory(directory) {
79
+ return existsSync(resolve(directory, "SKILL.md"));
80
+ }
81
+
82
+ function removeExisting(path, force) {
83
+ if (!existsSync(path)) return;
84
+ if (!force) {
85
+ throw new Error(
86
+ `${relativeDisplay(path)} ya existe. Usa --force para reemplazarlo.`,
87
+ );
88
+ }
89
+ rmSync(path, { recursive: true, force: true });
90
+ }
91
+
92
+ function installCanonical(force) {
93
+ if (resolve(sourceSkill) === canonicalSkill) return;
94
+
95
+ if (isSkillDirectory(canonicalSkill)) {
96
+ if (skillDigest(canonicalSkill) === skillDigest(sourceSkill)) return;
97
+ removeExisting(canonicalSkill, force);
98
+ } else if (existsSync(canonicalSkill)) {
99
+ removeExisting(canonicalSkill, force);
100
+ }
101
+
102
+ mkdirSync(dirname(canonicalSkill), { recursive: true });
103
+ cpSync(sourceSkill, canonicalSkill, { recursive: true });
104
+ }
105
+
106
+ function installAdapter(agent, force) {
107
+ const target = adapterPaths[agent];
108
+ if (!target || target === canonicalSkill) return;
109
+
110
+ if (existsSync(target)) {
111
+ const stat = lstatSync(target);
112
+ if (stat.isSymbolicLink()) {
113
+ const resolvedLink = resolve(dirname(target), readlinkSync(target));
114
+ if (resolvedLink === canonicalSkill) return;
115
+ }
116
+ removeExisting(target, force);
117
+ }
118
+
119
+ mkdirSync(dirname(target), { recursive: true });
120
+ const targetReference =
121
+ process.platform === "win32"
122
+ ? canonicalSkill
123
+ : relative(dirname(target), canonicalSkill);
124
+ symlinkSync(
125
+ targetReference,
126
+ target,
127
+ process.platform === "win32" ? "junction" : "dir",
128
+ );
129
+ }
130
+
131
+ async function chooseAgent() {
132
+ const explicit = option("--agent");
133
+ if (explicit) return explicit;
134
+
135
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
136
+ throw new Error(
137
+ "Indica --agent antigravity, --agent claude, --agent codex o --agent all.",
138
+ );
139
+ }
140
+
141
+ const prompt = createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout,
144
+ });
145
+ const answer = await prompt.question(
146
+ "¿Qué agente usará la skill? [1] Antigravity [2] Claude Code [3] Codex [4] Todos: ",
147
+ );
148
+ prompt.close();
149
+
150
+ return (
151
+ { 1: "antigravity", 2: "claude", 3: "codex", 4: "all" }[answer.trim()] ??
152
+ answer.trim().toLowerCase()
153
+ );
154
+ }
155
+
156
+ function validateAgent(agent) {
157
+ if (!["antigravity", "claude", "codex", "all"].includes(agent)) {
158
+ throw new Error(`Agente no reconocido: ${agent || "(vacío)"}.`);
159
+ }
160
+ }
161
+
162
+ async function installSkill() {
163
+ const agent = await chooseAgent();
164
+ validateAgent(agent);
165
+ const force = process.argv.includes("--force");
166
+
167
+ installCanonical(force);
168
+ if (agent === "all") {
169
+ installAdapter("claude", force);
170
+ installAdapter("codex", force);
171
+ } else {
172
+ installAdapter(agent, force);
173
+ }
174
+
175
+ console.log("Skill Bacano Figma Web instalada.");
176
+ console.log(`Fuente canónica: ${relativeDisplay(canonicalSkill)}`);
177
+ if (agent === "claude" || agent === "all") {
178
+ console.log(`Claude Code: ${relativeDisplay(adapterPaths.claude)}`);
179
+ }
180
+ if (agent === "codex" || agent === "all") {
181
+ console.log(`Codex: ${relativeDisplay(adapterPaths.codex)}`);
182
+ }
183
+ console.log("Ejecuta bacano-figma-web doctor para revisar la conexión.");
184
+ }
185
+
186
+ function checkPath(name, path) {
187
+ if (!existsSync(path)) {
188
+ return { name, path: relativeDisplay(path), status: "missing" };
189
+ }
190
+ if (!isSkillDirectory(path)) {
191
+ return { name, path: relativeDisplay(path), status: "invalid" };
192
+ }
193
+ return {
194
+ name,
195
+ path: relativeDisplay(path),
196
+ status:
197
+ skillDigest(path) === skillDigest(sourceSkill) ? "current" : "outdated",
198
+ link: lstatSync(path).isSymbolicLink(),
199
+ };
200
+ }
201
+
202
+ function skillStatus() {
203
+ const results = [
204
+ checkPath("canonical", canonicalSkill),
205
+ checkPath("antigravity", adapterPaths.antigravity),
206
+ checkPath("claude", adapterPaths.claude),
207
+ checkPath("codex", adapterPaths.codex),
208
+ ];
209
+ return results.filter(
210
+ (result, index) =>
211
+ results.findIndex((candidate) => candidate.path === result.path) ===
212
+ index,
213
+ );
214
+ }
215
+
216
+ function check() {
217
+ const results = skillStatus();
218
+ console.table(results);
219
+ if (results[0]?.status !== "current") {
220
+ console.error(
221
+ "Bacano Figma Web no está instalada o está desactualizada. Ejecuta bacano-figma-web install.",
222
+ );
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+ if (results.slice(1).some((result) => result.link === false)) {
227
+ console.warn(
228
+ "Hay copias físicas en adaptadores. Reinstala con --force para evitar divergencias.",
229
+ );
230
+ }
231
+ if (results.some((result) => result.status === "outdated")) {
232
+ process.exitCode = 1;
233
+ }
234
+ }
235
+
236
+ function canConnect(port, timeout = 250) {
237
+ return new Promise((finish) => {
238
+ const socket = connect({ host: "127.0.0.1", port });
239
+ const close = (available) => {
240
+ socket.destroy();
241
+ finish(available);
242
+ };
243
+ socket.setTimeout(timeout);
244
+ socket.once("connect", () => close(true));
245
+ socket.once("timeout", () => close(false));
246
+ socket.once("error", () => close(false));
247
+ });
248
+ }
249
+
250
+ async function doctor() {
251
+ const bridgeChecks = await Promise.all(
252
+ Array.from({ length: 10 }, (_, index) => 9223 + index).map(
253
+ async (port) => ({
254
+ port,
255
+ available: await canConnect(port),
256
+ }),
257
+ ),
258
+ );
259
+ const report = {
260
+ skill: skillStatus()[0]?.status ?? "missing",
261
+ officialRemoteMcp: "verify-in-agent",
262
+ officialDesktopMcp: (await canConnect(3845))
263
+ ? "detected-on-127.0.0.1:3845"
264
+ : "not-detected",
265
+ communityBridgePorts: bridgeChecks
266
+ .filter((result) => result.available)
267
+ .map((result) => result.port),
268
+ restToken: process.env.FIGMA_ACCESS_TOKEN ? "configured" : "not-configured",
269
+ };
270
+
271
+ if (process.argv.includes("--json")) {
272
+ console.log(JSON.stringify(report, null, 2));
273
+ return;
274
+ }
275
+
276
+ console.log("Diagnóstico Bacano Figma Web");
277
+ console.table([
278
+ { control: "Skill instalada", result: report.skill },
279
+ {
280
+ control: "MCP remoto oficial",
281
+ result: "Verificar OAuth y whoami dentro del agente",
282
+ },
283
+ { control: "MCP local oficial", result: report.officialDesktopMcp },
284
+ {
285
+ control: "Desktop Bridge comunitario",
286
+ result: report.communityBridgePorts.length
287
+ ? `Puerto(s) detectado(s): ${report.communityBridgePorts.join(", ")}`
288
+ : "No detectado",
289
+ },
290
+ { control: "Token REST (solo presencia)", result: report.restToken },
291
+ ]);
292
+ console.log(
293
+ "Este diagnóstico no lee secretos ni puede certificar el OAuth del cliente MCP.",
294
+ );
295
+ }
296
+
297
+ const command = process.argv[2] ?? "help";
298
+
299
+ try {
300
+ if (command === "install") await installSkill();
301
+ else if (command === "check") check();
302
+ else if (command === "doctor") await doctor();
303
+ else if (command === "validate-snapshot")
304
+ validateSnapshot(process.argv[3], projectRoot);
305
+ else usage();
306
+ } catch (error) {
307
+ console.error(error instanceof Error ? error.message : String(error));
308
+ process.exitCode = 1;
309
+ }
@@ -0,0 +1,139 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { relative, resolve } from "node:path";
3
+
4
+ function object(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ function validateFrame(frame, path, errors) {
9
+ if (!object(frame)) {
10
+ errors.push(`${path} debe ser un objeto.`);
11
+ return;
12
+ }
13
+ if (typeof frame.nodeId !== "string" && typeof frame.url !== "string") {
14
+ errors.push(`${path} requiere nodeId o url.`);
15
+ }
16
+ }
17
+
18
+ function inspectSecrets(value, errors, path = "manifest") {
19
+ const secretNames = new Set([
20
+ "accesstoken",
21
+ "authorization",
22
+ "cookie",
23
+ "password",
24
+ "personalaccesstoken",
25
+ "secret",
26
+ ]);
27
+ if (Array.isArray(value)) {
28
+ value.forEach((item, index) =>
29
+ inspectSecrets(item, errors, `${path}[${index}]`),
30
+ );
31
+ return;
32
+ }
33
+ if (!object(value)) return;
34
+ for (const [key, child] of Object.entries(value)) {
35
+ if (secretNames.has(key.toLowerCase())) {
36
+ errors.push(`${path}.${key} no puede contener secretos.`);
37
+ }
38
+ inspectSecrets(child, errors, `${path}.${key}`);
39
+ }
40
+ }
41
+
42
+ export function validateManifest(manifest) {
43
+ const errors = [];
44
+ const adapters = new Set([
45
+ "figma-official",
46
+ "figma-desktop",
47
+ "desktop-bridge",
48
+ "rest-snapshot",
49
+ ]);
50
+ const capabilities = new Set(["available", "partial", "missing"]);
51
+ const pageStatuses = new Set([
52
+ "pending",
53
+ "extracted",
54
+ "implemented",
55
+ "validated",
56
+ ]);
57
+
58
+ if (!object(manifest)) return ["El manifiesto debe ser un objeto JSON."];
59
+ if (manifest.schemaVersion !== 1) errors.push("schemaVersion debe ser 1.");
60
+ if (typeof manifest.project !== "string" || !manifest.project.trim()) {
61
+ errors.push("project es obligatorio.");
62
+ }
63
+ if (!object(manifest.source)) {
64
+ errors.push("source es obligatorio.");
65
+ } else {
66
+ if (!adapters.has(manifest.source.adapter)) {
67
+ errors.push("source.adapter no es compatible.");
68
+ }
69
+ if (
70
+ typeof manifest.source.extractedAt !== "string" ||
71
+ Number.isNaN(Date.parse(manifest.source.extractedAt))
72
+ ) {
73
+ errors.push("source.extractedAt debe ser una fecha ISO válida.");
74
+ }
75
+ }
76
+ if (manifest.capabilities !== undefined) {
77
+ if (!object(manifest.capabilities)) {
78
+ errors.push("capabilities debe ser un objeto.");
79
+ } else {
80
+ for (const [name, status] of Object.entries(manifest.capabilities)) {
81
+ if (!capabilities.has(status)) {
82
+ errors.push(`capabilities.${name} no tiene un estado válido.`);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ if (!Array.isArray(manifest.pages) || manifest.pages.length === 0) {
88
+ errors.push("pages requiere al menos una página.");
89
+ } else {
90
+ const routes = new Set();
91
+ manifest.pages.forEach((page, index) => {
92
+ const path = `pages[${index}]`;
93
+ if (!object(page)) {
94
+ errors.push(`${path} debe ser un objeto.`);
95
+ return;
96
+ }
97
+ if (typeof page.route !== "string" || !page.route.startsWith("/")) {
98
+ errors.push(`${path}.route debe comenzar por /.`);
99
+ } else if (routes.has(page.route)) {
100
+ errors.push(`${path}.route está duplicada.`);
101
+ } else {
102
+ routes.add(page.route);
103
+ }
104
+ if (typeof page.name !== "string" || !page.name.trim()) {
105
+ errors.push(`${path}.name es obligatorio.`);
106
+ }
107
+ if (!pageStatuses.has(page.status)) {
108
+ errors.push(`${path}.status no es válido.`);
109
+ }
110
+ if (!page.desktop && !page.mobile) {
111
+ errors.push(`${path} requiere un frame desktop o mobile.`);
112
+ }
113
+ if (page.desktop) validateFrame(page.desktop, `${path}.desktop`, errors);
114
+ if (page.mobile) validateFrame(page.mobile, `${path}.mobile`, errors);
115
+ });
116
+ }
117
+ inspectSecrets(manifest, errors);
118
+ return errors;
119
+ }
120
+
121
+ export function validateSnapshot(path, projectRoot) {
122
+ if (!path) throw new Error("Indica la ruta de manifest.json.");
123
+ const manifestPath = resolve(projectRoot, path);
124
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
125
+ throw new Error(`No existe el manifiesto: ${path}`);
126
+ }
127
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
128
+ const errors = validateManifest(manifest);
129
+ if (errors.length) {
130
+ console.error("Snapshot inválido:");
131
+ for (const error of errors) console.error(`- ${error}`);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+ console.log(`Snapshot válido: ${relative(projectRoot, manifestPath)}`);
136
+ console.log(
137
+ `${manifest.pages.length} página(s), fuente ${manifest.source.adapter}.`,
138
+ );
139
+ }
@@ -4,5 +4,9 @@ if (!process.env.CI && process.env.BACANO_SDK_HIDE_SKILL_NOTICE !== "1") {
4
4
  Instálala una vez: pnpm exec bacano-web install --agent all
5
5
  Verifica su estado: pnpm exec bacano-web check
6
6
  Luego invócala desde tu agente para iniciar la auditoría.
7
+
8
+ Para implementar un diseño Figma con el cascarón Bacano:
9
+ Instala la skill: pnpm exec bacano-figma-web install --agent all
10
+ Revisa la conexión: pnpm exec bacano-figma-web doctor
7
11
  `);
8
12
  }