Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,38 @@ Extensión de VS Code de un único fichero fuente (`src/extension.ts`). TypeScri
- `getEditorAndSelection()` obtiene editor activo + selección (helper compartido).
- `replaceSelection()` / `replaceWholeDocument()` aplican ediciones vía `editor.edit()`.

Los comandos Base64 y `decodeJwt` requieren selección. `fixJson` opera sobre la selección si la hay o sobre el documento completo en caso contrario.

Los cuatro comandos aparecen en el menú contextual del editor; Base64 y JWT solo cuando hay selección (`when: editorHasSelection`).

### Comando `fixJson` — pipeline de limpieza

La función `cleanAndParseJson()` prueba estrategias en orden hasta que `JSON.parse()` tenga éxito:
La función `cleanAndParseJson()` prueba estrategias en orden hasta que `JSON.parse()` tenga éxito. Si ninguna funciona, reemplaza el contenido con el último intento para que el usuario pueda inspeccionarlo.

| # | Estrategia |
|---|------------|
| 1 | Texto tal cual |
| 2 | Trim |
| 3 | `tryUnwrapString` — desenvuelve capa exterior `"…"` vía `JSON.parse` |
| 4 | `wrapAndUnescape` — envuelve en comillas para que el motor desescapee `\"` en un solo paso; falla si el texto contiene comillas sin escapar |
| 5 | Eliminación de barras antes de comillas (sin contexto URL) + `\"` → `"` |
| 6 | Eliminación de barras simétricas `/"key/"` en ambos lados |
| 7 | `grafanaDeepClean` — BOM, CRLF, desescapado multi-nivel, trailing commas, `unquoteJsonStringValues` |
| 8 | `grafanaDeepClean` + `tryUnwrapString` combinados |

1. Texto tal cual
2. Trim
3. `tryUnwrapString` — desenvuelve capa exterior `"…"` vía `JSON.parse`
4. `wrapAndUnescape` — envuelve en comillas para que el motor desescapee `\"`
5. Eliminación de barras antes de comillas (sin contexto URL)
6. Eliminación de barras simétricas `/"key/"` en ambos lados
7. `grafanaDeepClean` — BOM, CRLF, desescapado multi-nivel, trailing commas, `unquoteJsonStringValues`
8. `grafanaDeepClean` + `tryUnwrapString` combinados
#### Helpers clave del pipeline

El formateado final usa `formatJsonText()`, un formateador a nivel de texto que **no pasa por `JSON.parse` → `JSON.stringify`**, preservando así representaciones numéricas como `150.0`.
- **`tryUnwrapString`** — si el string empieza y termina con `"`, usa `JSON.parse` para desescapar; hace fallback a recorte manual.
- **`grafanaDeepClean`** — limpiador multi-paso: BOM → CRLF → unwrap (dos niveles) → barras simétricas → barras asimétricas → desescapado iterativo (máx. 5 pasadas) → segunda pasada de barras → trailing slash en valores → `unquoteJsonStringValues` → contenedores vacíos como string → dobles comillas exteriores → trailing commas.
- **`unquoteJsonStringValues`** — elimina las comillas que envuelven valores que son objetos/arrays JSON sin escapar (p. ej. `"Content":"{"k":"v"}"` → `"Content":{"k":"v"}`). Itera hasta estabilización.

#### Formateador de texto `formatJsonText`

Formatea JSON con 2 espacios de indentación **trabajando a nivel de texto**, sin pasar por `JSON.parse` → `JSON.stringify`. Esto preserva representaciones numéricas como `150.0`. Los contenedores vacíos (`{}`, `[]`) se emiten en una sola línea.

## Release

Empujar un tag `vX.Y.Z` dispara el workflow `.github/workflows/build.yml`, que sincroniza la versión en `package.json`, empaqueta el `.vsix` y lo adjunta al GitHub Release (lo crea si no existe).
El workflow `.github/workflows/build.yml` se dispara en dos situaciones:

- **Push a `main`**: compila y sube el `.vsix` como artefacto de CI (90 días de retención), pero no crea release.
- **Push de tag `vX.Y.Z`**: además sincroniza la versión en `package.json` y adjunta el `.vsix` al GitHub Release (lo crea si no existe).
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
{
"command": "wrenchit.fixJson",
"title": "WrenchIT: Limpiar y formatear JSON (Grafana)"
},
{
"command": "wrenchit.decodeJwt",
"title": "WrenchIT: Decodificar JWT"
}
],
"menus": {
Expand All @@ -43,6 +47,11 @@
{
"command": "wrenchit.fixJson",
"group": "wrenchit@3"
},
{
"command": "wrenchit.decodeJwt",
"group": "wrenchit@4",
"when": "editorHasSelection"
}
]
}
Expand Down
46 changes: 45 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('wrenchit.encodeBase64', () => encodeBase64()),
vscode.commands.registerCommand('wrenchit.decodeBase64', () => decodeBase64()),
vscode.commands.registerCommand('wrenchit.fixJson', () => fixJson())
vscode.commands.registerCommand('wrenchit.fixJson', () => fixJson()),
vscode.commands.registerCommand('wrenchit.decodeJwt', () => decodeJwt())
);
}

Expand Down Expand Up @@ -371,6 +372,49 @@ function grafanaDeepClean(s: string): string {
return result;
}

// ---------------------------------------------------------------------------
// Decode JWT
// ---------------------------------------------------------------------------

function decodeJwt() {
const ctx = getEditorAndSelection();
if (!ctx) { return; }
const { editor, selection, text } = ctx;

if (!text.trim()) {
vscode.window.showWarningMessage('WrenchIT: Selecciona el token JWT que quieres decodificar.');
return;
}

const parts = text.trim().split('.');
if (parts.length < 2) {
vscode.window.showErrorMessage('WrenchIT: El texto seleccionado no tiene formato JWT (se esperan al menos 2 partes separadas por ".").');
return;
}

try {
const decoded = decodeJwtParts(parts[0], parts[1]);
replaceSelection(editor, selection, decoded);
} catch {
vscode.window.showErrorMessage('WrenchIT: No se pudo decodificar el JWT. Comprueba que el token es válido.');
}
}

function decodeJwtParts(headerB64: string, payloadB64: string): string {
const decodeSegment = (seg: string): unknown => {
// Base64url → Base64 standard → Buffer → UTF-8 → JSON
const base64 = seg.replace(/-/g, '+').replace(/_/g, '/');
const json = Buffer.from(base64, 'base64').toString('utf8');
return JSON.parse(json);
};

const header = decodeSegment(headerB64);
const payload = decodeSegment(payloadB64);

const combined = JSON.stringify({ header, payload });
return formatJsonText(combined);
}

// ---------------------------------------------------------------------------
// Text-level JSON formatter (preserves original numeric representations)
// ---------------------------------------------------------------------------
Expand Down
Loading