agentic-workflow-manager 3.13.4 → 3.13.5
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/dist/src/core/discovery.js +13 -21
- package/dist/src/core/export/transform.js +30 -25
- package/dist/src/core/frontmatter.js +223 -0
- package/dist/src/core/renderers/canonical-agent.js +33 -2
- package/dist/src/core/renderers/skill-source.js +11 -19
- package/dist/src/ui/text.js +10 -2
- package/dist/tests/core/discovery.test.js +10 -1
- package/dist/tests/core/export/transform.test.js +110 -21
- package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
- package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
- package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
- package/dist/tests/ui/text.test.js +9 -0
- package/package.json +3 -1
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.matchFrontmatterBlock =
|
|
6
|
+
exports.findFrontmatterDescription = exports.isBlockScalarHeader = exports.readFrontmatterDescription = exports.matchFrontmatterBlock = void 0;
|
|
7
7
|
exports.readArtifactDescription = readArtifactDescription;
|
|
8
8
|
exports.discoverSkills = discoverSkills;
|
|
9
9
|
exports.discoverWorkflows = discoverWorkflows;
|
|
@@ -12,31 +12,23 @@ exports.discoverAgents = discoverAgents;
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const registries_1 = require("./registries");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// El parseo de frontmatter vive en el modulo HOJA `core/frontmatter.ts`
|
|
16
|
+
// (sin imports, para que consumidores puros como export/transform.ts no
|
|
17
|
+
// arrastren fs/git por transitividad). Se re-exporta aca porque este modulo
|
|
18
|
+
// ya era el punto de entrada historico para esos helpers.
|
|
19
|
+
var frontmatter_1 = require("./frontmatter");
|
|
20
|
+
Object.defineProperty(exports, "matchFrontmatterBlock", { enumerable: true, get: function () { return frontmatter_1.matchFrontmatterBlock; } });
|
|
21
|
+
Object.defineProperty(exports, "readFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.readFrontmatterDescription; } });
|
|
22
|
+
Object.defineProperty(exports, "isBlockScalarHeader", { enumerable: true, get: function () { return frontmatter_1.isBlockScalarHeader; } });
|
|
23
|
+
Object.defineProperty(exports, "findFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.findFrontmatterDescription; } });
|
|
24
|
+
const frontmatter_2 = require("./frontmatter");
|
|
20
25
|
function readArtifactDescription(filePath) {
|
|
21
26
|
try {
|
|
22
27
|
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
23
|
-
const frontmatter = matchFrontmatterBlock(raw);
|
|
28
|
+
const frontmatter = (0, frontmatter_2.matchFrontmatterBlock)(raw);
|
|
24
29
|
if (frontmatter === null)
|
|
25
30
|
return '';
|
|
26
|
-
|
|
27
|
-
.split(/\r?\n/)
|
|
28
|
-
.find((l) => /^description\s*:/.test(l));
|
|
29
|
-
if (!line)
|
|
30
|
-
return '';
|
|
31
|
-
let val = line.replace(/^description\s*:/, '').trim();
|
|
32
|
-
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
33
|
-
(val.startsWith("'") && val.endsWith("'"))) {
|
|
34
|
-
val = val.slice(1, -1);
|
|
35
|
-
}
|
|
36
|
-
const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
|
|
37
|
-
if (BLOCK_INDICATORS.has(val.trim()))
|
|
38
|
-
return '';
|
|
39
|
-
return val.trim();
|
|
31
|
+
return (0, frontmatter_2.readFrontmatterDescription)(frontmatter);
|
|
40
32
|
}
|
|
41
33
|
catch {
|
|
42
34
|
return '';
|
|
@@ -7,7 +7,10 @@ exports.claudeAiTransform = claudeAiTransform;
|
|
|
7
7
|
//
|
|
8
8
|
// Transform mecánico claude.ai (R3.1): función pura string → string.
|
|
9
9
|
// Frontmatter line-based plano (los SKILL.md del baseline usan claves de una
|
|
10
|
-
// línea) — sin parser YAML a propósito (YAGNI, cero deps).
|
|
10
|
+
// línea) — sin parser YAML a propósito (YAGNI, cero deps). La única forma
|
|
11
|
+
// multilínea soportada es el block scalar de `description:`, resuelto vía la
|
|
12
|
+
// función compartida de discovery.ts (ver claudeAiTransform).
|
|
13
|
+
const frontmatter_1 = require("../frontmatter");
|
|
11
14
|
const DEFERENCE_LINE = (skillName) => `In environments with AWM installed (Claude Code), defer to the registry's ${skillName} skill — this port is for environments without filesystem access.`;
|
|
12
15
|
exports.DEFERENCE_LINE = DEFERENCE_LINE;
|
|
13
16
|
// Paths intra-registry: resuelven en Claude Code (donde el registry está en
|
|
@@ -73,34 +76,36 @@ function claudeAiTransform(skillMd, skillName) {
|
|
|
73
76
|
const body = skillMd.slice(end + endMatch[0].length);
|
|
74
77
|
const fmLines = skillMd.slice(startLen, end).split(/\r?\n/)
|
|
75
78
|
.filter((l) => !/^(version|portable):/.test(l));
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
// UN SOLO camino para todas las formas de `description`. Antes habia uno por
|
|
80
|
+
// forma (plano / entrecomillado / bloque) y cada vez que el LECTOR aprendia
|
|
81
|
+
// una forma nueva, el ESCRITOR de aca quedaba atras — la divergencia exacta
|
|
82
|
+
// que este modulo dice cerrar. Casos reales que produjo esa asimetria:
|
|
83
|
+
// - escalar plano multilinea: la deference line se insertaba en la primera
|
|
84
|
+
// linea y la continuacion quedaba huerfana debajo, o sea la frase de
|
|
85
|
+
// deference terminaba enterrada en el medio de la descripcion;
|
|
86
|
+
// - escalar plano con ` # comentario` final: la deference line se anexaba
|
|
87
|
+
// DESPUES del `#`, o sea YAML se la comia entera como comentario y el
|
|
88
|
+
// artefacto exportado perdia en silencio la unica frase que este
|
|
89
|
+
// transform existe para agregar.
|
|
90
|
+
// Ahora se resuelve el campo con la funcion compartida, se reemplaza su
|
|
91
|
+
// extension COMPLETA (startLine..endLine) y se emite siempre un escalar
|
|
92
|
+
// double-quoted via JSON.stringify — superset valido de YAML que cubre
|
|
93
|
+
// comillas, `:`, `#` y los `\n` de un literal `|` sin decidir estilo.
|
|
94
|
+
const field = (0, frontmatter_1.findFrontmatterDescription)(fmLines.join('\n'));
|
|
95
|
+
if (field.startLine === -1) {
|
|
78
96
|
throw new Error('frontmatter has no description field');
|
|
79
97
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
const rawValue = fmLines[field.startLine].replace(/^description\s*:/, '').trim();
|
|
99
|
+
// Empieza con `>`/`|` pero no es un indicador bien formado (ej. `>-basura`):
|
|
100
|
+
// YAML mismo lo rechaza. Fallar explicito en vez de publicar basura.
|
|
101
|
+
if (/^[>|]/.test(rawValue) && !(0, frontmatter_1.isBlockScalarHeader)(rawValue)) {
|
|
102
|
+
throw new Error(`malformed block scalar indicator in description: ${rawValue}`);
|
|
84
103
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// single- and double-quoted scalars are first-class, and we work off the
|
|
88
|
-
// trimmed value so trailing whitespace after a closing quote doesn't fool us.
|
|
89
|
-
const isDoubleQuoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"');
|
|
90
|
-
const isSingleQuoted = value.length >= 2 && value.startsWith("'") && value.endsWith("'");
|
|
91
|
-
if ((value.startsWith('"') || value.startsWith("'")) && !isDoubleQuoted && !isSingleQuoted) {
|
|
92
|
-
throw new Error('description has trailing content after its closing quote (e.g. an inline comment) — not supported by the export transform; remove the comment or use a port.claude-ai.md override');
|
|
104
|
+
if (!field.value) {
|
|
105
|
+
throw new Error((0, frontmatter_1.isBlockScalarHeader)(rawValue) ? 'description block scalar has no content' : 'description is empty');
|
|
93
106
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// apostrophe, so it must be escaped before splicing into a single-quoted
|
|
97
|
-
// description or it would prematurely close the YAML string.
|
|
98
|
-
const newValue = isDoubleQuoted
|
|
99
|
-
? `${value.slice(0, -1)} ${deference}"`
|
|
100
|
-
: isSingleQuoted
|
|
101
|
-
? `${value.slice(0, -1)} ${deference.replace(/'/g, "''")}'`
|
|
102
|
-
: `${value} ${deference}`;
|
|
103
|
-
fmLines[descIdx] = `description: ${newValue}`;
|
|
107
|
+
const merged = JSON.stringify(`${field.value} ${(0, exports.DEFERENCE_LINE)(skillName)}`);
|
|
108
|
+
fmLines.splice(field.startLine, field.endLine - field.startLine + 1, `description: ${merged}`);
|
|
104
109
|
// Solo el body: el frontmatter ya se editó arriba y sus campos no son prosa
|
|
105
110
|
// navegable (R2.4).
|
|
106
111
|
return `---\n${fmLines.join('\n')}\n---\n${stripIntraRegistryPaths(body)}`;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/core/frontmatter.ts
|
|
3
|
+
//
|
|
4
|
+
// Modulo HOJA: sin imports. Es la unica fuente de verdad para leer el
|
|
5
|
+
// frontmatter YAML de un artefacto (SKILL.md, agents/*.md, workflows/*.md).
|
|
6
|
+
//
|
|
7
|
+
// Por que hoja: sus consumidores incluyen `core/export/transform.ts`, que se
|
|
8
|
+
// documenta como funcion pura string -> string sin dependencias. Si esto
|
|
9
|
+
// viviera en `discovery.ts` (que importa `registries.ts`, y este a su vez
|
|
10
|
+
// resuelve `awmHome()` y carga `simple-git` en tiempo de import), un modulo
|
|
11
|
+
// puro terminaria arrastrando la capa de fs/git por transitividad.
|
|
12
|
+
//
|
|
13
|
+
// Por que existe: el frontmatter se parseaba a mano en CUATRO lugares, cada
|
|
14
|
+
// uno con su propia copia incompleta. Un block scalar YAML valido
|
|
15
|
+
// (`description: >-`, con el texto en las lineas indentadas siguientes)
|
|
16
|
+
// degradaba en silencio en discovery, crasheaba `awm add -a cursor|copilot`,
|
|
17
|
+
// abortaba `awm export <bundle>` entero y rompia `awm add -a codex`. La
|
|
18
|
+
// semantica de aca se verifica contra js-yaml (parser real) en
|
|
19
|
+
// tests/core/frontmatter-description-vs-yaml.test.ts — no contra valores
|
|
20
|
+
// esperados escritos a mano, que pueden envejecer hacia la expectativa
|
|
21
|
+
// equivocada.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.matchFrontmatterBlock = matchFrontmatterBlock;
|
|
24
|
+
exports.isBlockScalarHeader = isBlockScalarHeader;
|
|
25
|
+
exports.findFrontmatterDescription = findFrontmatterDescription;
|
|
26
|
+
exports.readFrontmatterDescription = readFrontmatterDescription;
|
|
27
|
+
/** Texto crudo del frontmatter (entre los delimitadores `---`), o null si el
|
|
28
|
+
* bloque falta o esta mal formado. Tolera CRLF. */
|
|
29
|
+
function matchFrontmatterBlock(raw) {
|
|
30
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
31
|
+
return fmMatch ? fmMatch[1] : null;
|
|
32
|
+
}
|
|
33
|
+
/** Indicador de block scalar en la linea de la clave: estilo (`>` folded /
|
|
34
|
+
* `|` literal) + chomping opcional (`-`/`+`) + indentacion explicita opcional
|
|
35
|
+
* + comentario final opcional. Detalles verificados contra js-yaml:
|
|
36
|
+
* - la indentacion explicita es UN digito 1-9 (ni `0` ni `10`);
|
|
37
|
+
* - `>- # nota` es valido y su bloque se lee normal;
|
|
38
|
+
* - `>-basura` NO es valido y aca tampoco matchea. */
|
|
39
|
+
const BLOCK_SCALAR_HEADER = /^([>|])(?:([+-])?([1-9])?|([1-9])?([+-])?)(?:\s+#.*)?$/;
|
|
40
|
+
/** ¿El valor de la linea de la clave es un indicador de block scalar?
|
|
41
|
+
* Cualquier caller que RAMIFIQUE sobre "esto es un bloque" debe usar esta
|
|
42
|
+
* funcion, nunca una aproximacion propia: una guarda por prefijo (`/^[>|]/`)
|
|
43
|
+
* y un resolver por match completo se desincronizan, y en ese hueco el
|
|
44
|
+
* escritor borro contenido real creyendo que resolvia un bloque. */
|
|
45
|
+
function isBlockScalarHeader(value) {
|
|
46
|
+
return BLOCK_SCALAR_HEADER.test(value.trim());
|
|
47
|
+
}
|
|
48
|
+
/** Indice de la comilla que CIERRA el escalar que empieza en la posicion 0, o
|
|
49
|
+
* -1 si no cierra. Cada estilo escapa distinto: en single-quoted un `''` es
|
|
50
|
+
* un apostrofe literal (no un cierre); en double-quoted un `\"` es una
|
|
51
|
+
* comilla literal. */
|
|
52
|
+
function findClosingQuote(value, quote) {
|
|
53
|
+
for (let i = 1; i < value.length; i++) {
|
|
54
|
+
if (quote === '"' && value[i] === '\\') {
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (value[i] !== quote)
|
|
59
|
+
continue;
|
|
60
|
+
if (quote === "'" && value[i + 1] === "'") {
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
function foldBlock(stripped, isLiteral) {
|
|
69
|
+
if (isLiteral)
|
|
70
|
+
return stripped.join('\n').trim();
|
|
71
|
+
// Folded (`>`): las lineas al nivel base del bloque se pliegan entre si con
|
|
72
|
+
// UN espacio; una linea MAS indentada no se pliega (conserva su salto y su
|
|
73
|
+
// sangria — la regla que permite incrustar una lista dentro de un folded);
|
|
74
|
+
// y k lineas en blanco consecutivas producen k saltos de linea.
|
|
75
|
+
// Ojo: NO se hace trim por linea — YAML conserva los espacios finales de
|
|
76
|
+
// cada linea y el plegado agrega el suyo encima.
|
|
77
|
+
let out = '';
|
|
78
|
+
let prevLiteral = false;
|
|
79
|
+
let started = false;
|
|
80
|
+
let blanks = 0;
|
|
81
|
+
for (const line of stripped) {
|
|
82
|
+
if (line === '') {
|
|
83
|
+
blanks++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const literal = /^[ \t]/.test(line);
|
|
87
|
+
if (!started) {
|
|
88
|
+
out = line;
|
|
89
|
+
started = true;
|
|
90
|
+
prevLiteral = literal;
|
|
91
|
+
blanks = 0;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (blanks > 0) {
|
|
95
|
+
out += '\n'.repeat(blanks);
|
|
96
|
+
blanks = 0;
|
|
97
|
+
}
|
|
98
|
+
else if (literal || prevLiteral)
|
|
99
|
+
out += '\n';
|
|
100
|
+
else
|
|
101
|
+
out += ' ';
|
|
102
|
+
out += line;
|
|
103
|
+
prevLiteral = literal;
|
|
104
|
+
}
|
|
105
|
+
return out.trim();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Localiza y RESUELVE el campo `description` de un bloque de frontmatter,
|
|
109
|
+
* devolviendo tambien la extension exacta que ocupa (para que un escritor
|
|
110
|
+
* pueda reemplazarlo entero).
|
|
111
|
+
*
|
|
112
|
+
* Formas soportadas, todas contrastadas contra js-yaml: escalar plano (de una
|
|
113
|
+
* o varias lineas), escalar entrecomillado (simple y doble, con su escape
|
|
114
|
+
* propio deshecho), y block scalar folded/literal con chomping e indentacion
|
|
115
|
+
* explicita.
|
|
116
|
+
*/
|
|
117
|
+
function findFrontmatterDescription(frontmatter) {
|
|
118
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
119
|
+
// Ancla en COLUMNA 0 a proposito: `description` en un frontmatter es
|
|
120
|
+
// siempre clave de nivel superior. Un `^\s*` haria ganar a un
|
|
121
|
+
// `description:` INDENTADO — anidado en otro mapa, o una linea de
|
|
122
|
+
// CONTENIDO de otro block scalar que casualmente empiece asi.
|
|
123
|
+
const startLine = lines.findIndex((l) => /^description\s*:/.test(l));
|
|
124
|
+
if (startLine === -1)
|
|
125
|
+
return { value: '', startLine: -1, endLine: -1 };
|
|
126
|
+
const raw = lines[startLine].replace(/^description\s*:/, '').trim();
|
|
127
|
+
if (isBlockScalarHeader(raw)) {
|
|
128
|
+
const isLiteral = raw.trim().startsWith('|');
|
|
129
|
+
// El bloque son las lineas ESTRICTAMENTE indentadas respecto de la
|
|
130
|
+
// clave (columna 0). Las lineas en blanco no lo cortan: pertenecen a el.
|
|
131
|
+
const block = [];
|
|
132
|
+
let last = startLine;
|
|
133
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
134
|
+
if (lines[i].trim() === '') {
|
|
135
|
+
block.push(lines[i]);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!/^[ \t]/.test(lines[i]))
|
|
139
|
+
break;
|
|
140
|
+
block.push(lines[i]);
|
|
141
|
+
last = i;
|
|
142
|
+
}
|
|
143
|
+
while (block.length > 0 && block[block.length - 1].trim() === '')
|
|
144
|
+
block.pop();
|
|
145
|
+
const first = block.find((l) => l.trim() !== '');
|
|
146
|
+
if (first === undefined)
|
|
147
|
+
return { value: '', startLine, endLine: startLine };
|
|
148
|
+
const declared = raw.match(/[1-9]/);
|
|
149
|
+
const blockIndent = declared !== null ? Number(declared[0]) : first.match(/^[ \t]*/)[0].length;
|
|
150
|
+
// Una linea con MENOS indentacion que el bloque es YAML invalido
|
|
151
|
+
// (js-yaml lanza "bad indentation"). Cortar a ciegas con slice()
|
|
152
|
+
// comeria caracteres reales: preferimos no inventar un valor.
|
|
153
|
+
if (block.some((l) => l.trim() !== '' && l.match(/^[ \t]*/)[0].length < blockIndent)) {
|
|
154
|
+
return { value: '', startLine, endLine: last };
|
|
155
|
+
}
|
|
156
|
+
const stripped = block.map((l) => l.slice(blockIndent));
|
|
157
|
+
return { value: foldBlock(stripped, isLiteral), startLine, endLine: last };
|
|
158
|
+
}
|
|
159
|
+
// Empieza con `>`/`|` pero NO es un indicador bien formado (`>0`, `>12`,
|
|
160
|
+
// `>-basura`): YAML lo rechaza. Devolver '' hace que los consumidores
|
|
161
|
+
// fallen fuerte (throw en parseSkillSource / claudeAiTransform) en vez de
|
|
162
|
+
// caer al camino de escalar plano de abajo, que publicaria el indicador
|
|
163
|
+
// mismo como si fuera la descripcion.
|
|
164
|
+
if (/^[>|]/.test(raw))
|
|
165
|
+
return { value: '', startLine, endLine: startLine };
|
|
166
|
+
// Continuacion multilinea. Aplica a DOS formas, y ambas la necesitan:
|
|
167
|
+
// - escalar plano (`description: primera` + lineas indentadas), que YAML
|
|
168
|
+
// pliega con espacios igual que un `>`;
|
|
169
|
+
// - escalar ENTRECOMILLADO cuya comilla de cierre esta en una linea
|
|
170
|
+
// posterior (`description: "hola` / ` mundo"`), que sin esto devolvia
|
|
171
|
+
// `"hola` — con la comilla de apertura pegada — como descripcion.
|
|
172
|
+
// Sin juntar las lineas primero, del lado escritor ademas quedaban
|
|
173
|
+
// huerfanas en la salida exportada.
|
|
174
|
+
let endLine = startLine;
|
|
175
|
+
let value = raw;
|
|
176
|
+
const opensUnclosedQuote = /^['"]/.test(raw) && findClosingQuote(raw, raw[0]) === -1;
|
|
177
|
+
if (raw !== '' && (!/^['"]/.test(raw) || opensUnclosedQuote)) {
|
|
178
|
+
const cont = [];
|
|
179
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
180
|
+
if (lines[i].trim() === '')
|
|
181
|
+
break;
|
|
182
|
+
if (!/^[ \t]/.test(lines[i]))
|
|
183
|
+
break;
|
|
184
|
+
cont.push(lines[i].trim());
|
|
185
|
+
endLine = i;
|
|
186
|
+
}
|
|
187
|
+
if (cont.length > 0)
|
|
188
|
+
value = [raw, ...cont].join(' ');
|
|
189
|
+
}
|
|
190
|
+
// Escalares entrecomillados. Dos cosas que no alcanzan por separado:
|
|
191
|
+
// 1. Hay que ENCONTRAR la comilla de cierre real antes de mirar el resto:
|
|
192
|
+
// lo que sigue a esa comilla es un comentario, no parte del valor.
|
|
193
|
+
// Comparar con `endsWith` fallaba justo cuando habia comentario
|
|
194
|
+
// (`description: "x" # nota` devolvia `"x"` CON las comillas literales,
|
|
195
|
+
// mientras js-yaml devuelve `x`).
|
|
196
|
+
// 2. Hay que deshacer el escape propio de cada estilo: single-quoted
|
|
197
|
+
// duplica el apostrofe (`''`); double-quoted es superset de la string
|
|
198
|
+
// JSON, asi que JSON.parse resuelve \n, \t, \", \\ y \uXXXX.
|
|
199
|
+
const quote = value[0];
|
|
200
|
+
if (quote === "'" || quote === '"') {
|
|
201
|
+
const close = findClosingQuote(value, quote);
|
|
202
|
+
if (close !== -1) {
|
|
203
|
+
const scalar = value.slice(0, close + 1);
|
|
204
|
+
if (quote === "'") {
|
|
205
|
+
return { value: scalar.slice(1, -1).replace(/''/g, "'").trim(), startLine, endLine };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return { value: String(JSON.parse(scalar)).trim(), startLine, endLine };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return { value: scalar.slice(1, -1).trim(), startLine, endLine };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Escalar plano: un `#` PRECEDIDO DE ESPACIO abre un comentario y no forma
|
|
216
|
+
// parte del valor (un `#` pegado a texto, como en `C#`, si). Dentro de
|
|
217
|
+
// comillas el `#` es literal, y esas ramas ya retornaron.
|
|
218
|
+
return { value: value.replace(/\s+#.*$/, '').trim(), startLine, endLine };
|
|
219
|
+
}
|
|
220
|
+
/** Azucar para el caso comun: solo el valor resuelto de `description`. */
|
|
221
|
+
function readFrontmatterDescription(frontmatter) {
|
|
222
|
+
return findFrontmatterDescription(frontmatter).value;
|
|
223
|
+
}
|
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseCanonicalAgent = parseCanonicalAgent;
|
|
4
|
+
const frontmatter_1 = require("../frontmatter");
|
|
4
5
|
function parseCanonicalAgent(source) {
|
|
5
6
|
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
6
7
|
if (!match)
|
|
7
8
|
throw new Error('canonical agent requires YAML frontmatter');
|
|
8
9
|
const fields = new Map();
|
|
9
|
-
|
|
10
|
+
// La validacion linea-por-linea sigue siendo estricta a proposito (atrapa
|
|
11
|
+
// frontmatter malformado), pero debe reconocer los block scalars: sus
|
|
12
|
+
// lineas de CONTENIDO estan indentadas y no son pares `clave: valor`.
|
|
13
|
+
// Antes lanzaban "invalid canonical agent frontmatter line", asi que un
|
|
14
|
+
// agente con `description: >-` rompia `awm add <x> -a codex`. Peor aun
|
|
15
|
+
// tras unificar el discovery: el picker mostraba la descripcion bien
|
|
16
|
+
// resuelta y recien despues explotaba el install — dos caminos leyendo el
|
|
17
|
+
// MISMO archivo con reglas distintas.
|
|
18
|
+
const lines = match[1].split(/\r?\n/);
|
|
19
|
+
let sawKey = false;
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
// Toda linea INDENTADA (o en blanco) es continuacion del valor de la
|
|
22
|
+
// clave anterior, no una clave nueva: asi se ven tanto el contenido de
|
|
23
|
+
// un block scalar como el de un escalar plano multilinea, y ambos son
|
|
24
|
+
// YAML valido. El primer fix de esto solo contemplo el block scalar,
|
|
25
|
+
// asi que `description: primera\n continuacion` seguia lanzando
|
|
26
|
+
// "invalid canonical agent frontmatter line" y rompiendo
|
|
27
|
+
// `awm add -a codex` — mientras discovery, leyendo el MISMO archivo con
|
|
28
|
+
// la funcion compartida, mostraba la descripcion correcta en el picker.
|
|
29
|
+
// Dos caminos sobre el mismo archivo o se arreglan juntos o divergen de
|
|
30
|
+
// una forma mas confusa que el bug original.
|
|
31
|
+
if (sawKey && (line.trim() === '' || /^[ \t]/.test(line)))
|
|
32
|
+
continue;
|
|
10
33
|
const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
11
34
|
if (!field)
|
|
12
35
|
throw new Error(`invalid canonical agent frontmatter line: ${line}`);
|
|
13
36
|
if (fields.has(field[1]))
|
|
14
37
|
throw new Error(`duplicate canonical agent frontmatter key: ${field[1]}`);
|
|
38
|
+
sawKey = true;
|
|
39
|
+
if ((0, frontmatter_1.isBlockScalarHeader)(field[2])) {
|
|
40
|
+
fields.set(field[1], '');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
15
43
|
fields.set(field[1], field[2].replace(/^(['"])(.*)\1$/, '$2').trim());
|
|
16
44
|
}
|
|
17
45
|
const name = fields.get('name') ?? '';
|
|
18
|
-
|
|
46
|
+
// La descripcion sale de la funcion compartida (resuelve block scalars y
|
|
47
|
+
// deshace el escape de los escalares entrecomillados) — nunca del mapa de
|
|
48
|
+
// arriba, que solo corta comillas.
|
|
49
|
+
const description = (0, frontmatter_1.readFrontmatterDescription)(match[1]);
|
|
19
50
|
const instructions = match[2].trim();
|
|
20
51
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
21
52
|
throw new Error('invalid agent name');
|
|
@@ -10,7 +10,7 @@ exports.parseSkillSource = parseSkillSource;
|
|
|
10
10
|
// link. Reuses discovery.ts's `matchFrontmatterBlock` (already the single
|
|
11
11
|
// source of truth for locating the frontmatter block elsewhere in this
|
|
12
12
|
// codebase) rather than writing a second frontmatter parser.
|
|
13
|
-
const
|
|
13
|
+
const frontmatter_1 = require("../frontmatter");
|
|
14
14
|
/**
|
|
15
15
|
* Parses a raw SKILL.md source into its `description` frontmatter field and
|
|
16
16
|
* body content. Mirrors discovery.ts's `readArtifactDescription` for the
|
|
@@ -20,26 +20,18 @@ const discovery_1 = require("../discovery");
|
|
|
20
20
|
* empty description or body.
|
|
21
21
|
*/
|
|
22
22
|
function parseSkillSource(source) {
|
|
23
|
-
const frontmatter = (0,
|
|
23
|
+
const frontmatter = (0, frontmatter_1.matchFrontmatterBlock)(source);
|
|
24
24
|
if (frontmatter === null)
|
|
25
25
|
throw new Error('skill source requires YAML frontmatter');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// real description text lives on the FOLLOWING indented lines, not on this
|
|
36
|
-
// line at all — treating the bare indicator as the description would embed
|
|
37
|
-
// literal "|-" into every rendered skill. Mirrors discovery.ts's
|
|
38
|
-
// readArtifactDescription, which detects the same shape and treats it as
|
|
39
|
-
// absent rather than mis-parsing it.
|
|
40
|
-
const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
|
|
41
|
-
if (BLOCK_INDICATORS.has(description))
|
|
42
|
-
description = '';
|
|
26
|
+
// Delega en readFrontmatterDescription (discovery.ts) — la MISMA funcion
|
|
27
|
+
// que usa el discovery del CLI, incluyendo la resolucion de block scalars
|
|
28
|
+
// (`description: >-` + lineas indentadas). Antes esta funcion tenia su
|
|
29
|
+
// propia copia de la logica, y ambas colapsaban el indicador de block
|
|
30
|
+
// scalar a '' en vez de leer el texto de las lineas siguientes: eso
|
|
31
|
+
// crasheaba `awm add <bundle> -a copilot|cursor` contra un skill real del
|
|
32
|
+
// registry baseline. Una sola implementacion = no pueden volver a
|
|
33
|
+
// divergir.
|
|
34
|
+
const description = (0, frontmatter_1.readFrontmatterDescription)(frontmatter);
|
|
43
35
|
if (!description)
|
|
44
36
|
throw new Error('skill source requires a non-empty description');
|
|
45
37
|
const bodyMatch = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
package/dist/src/ui/text.js
CHANGED
|
@@ -30,11 +30,19 @@ function displayWidth(s) {
|
|
|
30
30
|
}
|
|
31
31
|
return w;
|
|
32
32
|
}
|
|
33
|
-
/** Truncate plain text to `width` cells, appending '…'. Strips ANSI — color AFTER truncating.
|
|
33
|
+
/** Truncate plain text to `width` cells, appending '…'. Strips ANSI — color AFTER truncating.
|
|
34
|
+
*
|
|
35
|
+
* Los saltos de linea se colapsan a un espacio: esto se usa para celdas de UNA
|
|
36
|
+
* linea (filas de `awm list`), y `displayWidth` cuenta un `\n` como ancho 1 en
|
|
37
|
+
* vez de reconocerlo como salto — o sea que una string multilinea podia pasar
|
|
38
|
+
* el chequeo de ancho y emitirse cruda, partiendo la fila y desalineando todo
|
|
39
|
+
* lo que viniera despues. Antes era inalcanzable; dejo de serlo cuando el
|
|
40
|
+
* lector de frontmatter aprendio a resolver block scalars literales (`|`), que
|
|
41
|
+
* producen saltos de linea REALES en la descripcion. */
|
|
34
42
|
function truncate(s, width) {
|
|
35
43
|
if (width <= 0)
|
|
36
44
|
return '';
|
|
37
|
-
const plain = stripAnsi(s);
|
|
45
|
+
const plain = stripAnsi(s).replace(/\s*\r?\n\s*/g, ' ');
|
|
38
46
|
if (displayWidth(plain) <= width)
|
|
39
47
|
return plain;
|
|
40
48
|
if (width === 1)
|
|
@@ -105,8 +105,17 @@ describe('Artifact Discovery', () => {
|
|
|
105
105
|
fs_1.default.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
|
|
106
106
|
expect((0, discovery_1.readArtifactDescription)('/missing/SKILL.md')).toBe('');
|
|
107
107
|
});
|
|
108
|
-
it('
|
|
108
|
+
it('resuelve un block scalar leyendo sus lineas indentadas (antes descartaba el texto y devolvia vacio)', () => {
|
|
109
|
+
// Este test afirmaba `''` sobre un fixture que contiene literalmente
|
|
110
|
+
// "actual text" — codificaba la degradacion silenciosa, no un
|
|
111
|
+
// contrato. Un `description: >-` es YAML valido y su texto vive en
|
|
112
|
+
// las lineas indentadas siguientes; descartarlo dejaba el skill sin
|
|
113
|
+
// descripcion en todo el discovery del CLI.
|
|
109
114
|
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\n actual text\n---\n');
|
|
115
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('actual text');
|
|
116
|
+
});
|
|
117
|
+
it('sigue devolviendo vacio si el indicador de block scalar no tiene lineas indentadas (genuinamente sin descripcion)', () => {
|
|
118
|
+
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\nname: otra-clave\n---\n');
|
|
110
119
|
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
111
120
|
});
|
|
112
121
|
});
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
3
7
|
const transform_1 = require("../../../src/core/export/transform");
|
|
4
8
|
const FM = (lines) => `---\n${lines.join('\n')}\n---\nBody line.\n`;
|
|
9
|
+
/** Reparsea el frontmatter EXPORTADO con un YAML real y devuelve su
|
|
10
|
+
* `description`. Lanza si la salida no es YAML valido — que es justamente lo
|
|
11
|
+
* que queremos que falle fuerte, en vez de asertar sobre el texto crudo y no
|
|
12
|
+
* enterarnos de que emitimos algo que ningun parser puede leer. */
|
|
13
|
+
function descriptionOf(exported) {
|
|
14
|
+
const fm = exported.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
15
|
+
if (!fm)
|
|
16
|
+
throw new Error('la salida no tiene bloque de frontmatter');
|
|
17
|
+
const parsed = js_yaml_1.default.load(fm[1]);
|
|
18
|
+
if (typeof parsed.description !== 'string')
|
|
19
|
+
throw new Error('description ausente o no-string en la salida');
|
|
20
|
+
return parsed.description;
|
|
21
|
+
}
|
|
5
22
|
describe('claudeAiTransform', () => {
|
|
6
23
|
it('strips version and portable, keeps other keys and body intact', () => {
|
|
7
24
|
const input = FM(['name: mermaid-diagrams', 'version: "1.0.0"', 'portable: true', 'description: "Guide."']);
|
|
@@ -16,18 +33,25 @@ describe('claudeAiTransform', () => {
|
|
|
16
33
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
17
34
|
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
18
35
|
});
|
|
36
|
+
// NOTA sobre el estilo de la salida: el transform ya no conserva el estilo de
|
|
37
|
+
// comillas del fuente — emite SIEMPRE un escalar double-quoted via
|
|
38
|
+
// JSON.stringify, sea cual sea la forma de entrada (plano, entrecomillado,
|
|
39
|
+
// block scalar, multilinea). Antes habia una rama por forma, y cada vez que
|
|
40
|
+
// el lector aprendia una forma nueva el escritor quedaba atras: asi se
|
|
41
|
+
// colaron una deference line enterrada en medio de una descripcion
|
|
42
|
+
// multilinea y otra tragada entera por un `#` de comentario. Un solo camino
|
|
43
|
+
// hace imposible esa clase de divergencia. El contrato que estos tests
|
|
44
|
+
// protegen — YAML valido que conserva descripcion + deference line — se
|
|
45
|
+
// cumple igual, y se verifica reparseando la salida con js-yaml mas abajo.
|
|
19
46
|
it('appends the deference line to an unquoted description', () => {
|
|
20
47
|
const input = FM(['name: x', 'portable: true', 'description: Does things.']);
|
|
21
48
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
22
|
-
expect(out).toContain(`description: Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
49
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
23
50
|
});
|
|
24
51
|
it('appends the deference line inside a single-quoted description', () => {
|
|
25
52
|
const input = FM(['name: x', 'portable: true', "description: 'Does things.'"]);
|
|
26
53
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
27
|
-
|
|
28
|
-
// even a fixture with no apostrophe of its own must see it doubled ('')
|
|
29
|
-
// per YAML single-quote escaping once spliced into a single-quoted scalar.
|
|
30
|
-
expect(out).toContain(`description: 'Does things. ${(0, transform_1.DEFERENCE_LINE)('x').replace(/'/g, "''")}'`);
|
|
54
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
31
55
|
});
|
|
32
56
|
it('appends the deference line inside a double-quoted description with trailing whitespace', () => {
|
|
33
57
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." ']);
|
|
@@ -50,26 +74,91 @@ describe('claudeAiTransform', () => {
|
|
|
50
74
|
it('throws on frontmatter without description', () => {
|
|
51
75
|
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true']), 'x')).toThrow(/description/);
|
|
52
76
|
});
|
|
53
|
-
it('
|
|
54
|
-
|
|
77
|
+
it('resuelve una descripcion en block scalar y le anexa la deference line (antes abortaba el export del bundle entero)', () => {
|
|
78
|
+
// Regresion real: esto lanzaba, y runExport propaga el throw — asi que UN
|
|
79
|
+
// skill del registry baseline con esta forma valida (extract-design-md)
|
|
80
|
+
// hacia fallar `awm export frontend` COMPLETO, no solo ese skill.
|
|
81
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text', ' segunda linea']), 'x');
|
|
82
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
83
|
+
expect(descLine).toBe(`description: ${JSON.stringify(`folded text segunda linea ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
84
|
+
// Las lineas indentadas del bloque se consumieron: no quedan sueltas.
|
|
85
|
+
expect(out).not.toContain(' folded text');
|
|
86
|
+
expect(out).not.toContain('description: >');
|
|
87
|
+
});
|
|
88
|
+
it('no absorbe las claves siguientes del frontmatter al consumir el bloque', () => {
|
|
89
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' solo esto', 'name: sigue-viva']), 'x');
|
|
90
|
+
expect(out).toContain('name: sigue-viva');
|
|
91
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
92
|
+
expect(descLine).toContain('solo esto');
|
|
93
|
+
expect(descLine).not.toContain('sigue-viva');
|
|
94
|
+
});
|
|
95
|
+
it('sigue lanzando si el block scalar no tiene contenido', () => {
|
|
96
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-']), 'x')).toThrow(/no content/);
|
|
97
|
+
});
|
|
98
|
+
it('lanza si description esta presente pero vacia (sin valor y sin bloque)', () => {
|
|
99
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description:']), 'x')).toThrow(/description is empty/);
|
|
55
100
|
});
|
|
56
|
-
it('
|
|
101
|
+
it('resuelve un block scalar CON comentario final sin perder el contenido (regresion: guarda por prefijo vs match completo)', () => {
|
|
102
|
+
// La guarda de la rama de bloque usaba un prefijo (/^[>|]/) mientras el
|
|
103
|
+
// resolver exigia match COMPLETO del indicador. Con `>- # nota` entraban en
|
|
104
|
+
// desacuerdo: se publicaba el indicador como descripcion y las lineas de
|
|
105
|
+
// contenido REALES se borraban del artefacto exportado, en silencio.
|
|
106
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >- # nota al margen', ' el texto real']), 'x');
|
|
107
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
108
|
+
expect(descLine).toContain('el texto real');
|
|
109
|
+
expect(descLine).not.toContain('# nota al margen');
|
|
110
|
+
expect(out).not.toContain('description: >-');
|
|
111
|
+
});
|
|
112
|
+
it('lanza ante un indicador de bloque malformado en vez de emitir YAML invalido', () => {
|
|
113
|
+
// `>-basura` lo rechaza el propio YAML. Tratarlo como escalar plano emitiria
|
|
114
|
+
// `description: >-basura ...`, invalido porque `>` abre un indicador.
|
|
115
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-basura', ' texto']), 'x'))
|
|
116
|
+
.toThrow(/malformed block scalar indicator/);
|
|
117
|
+
});
|
|
118
|
+
it('conserva la linea en blanco que separa el bloque de la clave siguiente', () => {
|
|
119
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' el texto', '', 'name: x']), 'x');
|
|
120
|
+
expect(out).toMatch(/description: .*\n\nname: x/);
|
|
121
|
+
});
|
|
122
|
+
it('no deja que el apostrofe de la deference line rompa el escalar emitido', () => {
|
|
123
|
+
// DEFERENCE_LINE siempre contiene un apostrofe ("registry's"). El fix
|
|
124
|
+
// original lo doblaba ('') porque la salida era single-quoted; hoy la
|
|
125
|
+
// salida es double-quoted, donde el apostrofe es un caracter comun. Lo
|
|
126
|
+
// que el test protege no es la convencion de comillas sino que el
|
|
127
|
+
// resultado sea YAML bien formado con el texto intacto — se verifica
|
|
128
|
+
// reparseando con js-yaml en vez de inspeccionar comillas a ojo.
|
|
57
129
|
const input = FM(['name: mermaid', 'portable: true', "description: 'Diagrams and flowcharts.'"]);
|
|
58
130
|
const out = (0, transform_1.claudeAiTransform)(input, 'mermaid');
|
|
59
|
-
|
|
60
|
-
expect(out).toContain("registry'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// quote or part of a doubled '' pair.
|
|
67
|
-
const body = descLine.slice('description: \''.length, -1);
|
|
68
|
-
expect(body.replace(/''/g, '')).not.toMatch(/'/);
|
|
69
|
-
});
|
|
70
|
-
it('throws when a quoted description has trailing content after its closing quote (e.g. inline comment)', () => {
|
|
131
|
+
expect(descriptionOf(out)).toBe(`Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid')}`);
|
|
132
|
+
expect(descriptionOf(out)).toContain("registry's mermaid skill");
|
|
133
|
+
});
|
|
134
|
+
it('resuelve un escalar entrecomillado con comentario final en vez de lanzar', () => {
|
|
135
|
+
// Antes lanzaba. Pero `description: "x" # nota` es YAML valido y js-yaml lo
|
|
136
|
+
// lee como "x": el comentario no forma parte del valor. Lanzar obligaba al
|
|
137
|
+
// autor a tocar un SKILL.md que no tenia nada malo.
|
|
71
138
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." # a comment']);
|
|
72
|
-
expect((
|
|
139
|
+
expect(descriptionOf((0, transform_1.claudeAiTransform)(input, 'x'))).toBe(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
140
|
+
});
|
|
141
|
+
describe('round-trip: la salida se reparsea con un YAML real (par lector+escritor)', () => {
|
|
142
|
+
// Los tests del lector, por si solos, no habrian detectado que el ESCRITOR
|
|
143
|
+
// quedaba atras cuando el lector aprendia una forma nueva. Estos casos
|
|
144
|
+
// pasan cada forma por el transform y REPARSEAN el resultado, que es donde
|
|
145
|
+
// se ve si la deference line sobrevivio y si el YAML sigue siendo valido.
|
|
146
|
+
it.each([
|
|
147
|
+
['plano', ['description: Does things.']],
|
|
148
|
+
['plano multilinea', ['description: primera parte', ' y su continuacion']],
|
|
149
|
+
['plano con comentario final', ['description: hola mundo # nota del autor']],
|
|
150
|
+
['single-quoted', ["description: 'con apostrofe: it''s'"]],
|
|
151
|
+
['double-quoted', ['description: "con dos puntos: si"']],
|
|
152
|
+
['folded', ['description: >-', ' primera', ' segunda']],
|
|
153
|
+
['literal', ['description: |-', ' primera', ' segunda']],
|
|
154
|
+
['folded con comentario en el indicador', ['description: >- # nota', ' el texto']],
|
|
155
|
+
])('%s', (_name, descLines) => {
|
|
156
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true', ...descLines]), 'x');
|
|
157
|
+
const desc = descriptionOf(out); // lanza si la salida no es YAML valido
|
|
158
|
+
// La deference line es la razon de existir de este transform: nunca puede
|
|
159
|
+
// perderse en un comentario ni quedar sepultada en el medio del texto.
|
|
160
|
+
expect(desc.endsWith((0, transform_1.DEFERENCE_LINE)('x'))).toBe(true);
|
|
161
|
+
});
|
|
73
162
|
});
|
|
74
163
|
it('cleans intra-registry paths in the body', () => {
|
|
75
164
|
const md = [
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// Test DIFERENCIAL: readFrontmatterDescription (parser a mano, cero deps de
|
|
7
|
+
// runtime) contra js-yaml (parser YAML real, devDependency solo de test).
|
|
8
|
+
//
|
|
9
|
+
// Por que existe: el repo parsea el frontmatter a mano a proposito (ver
|
|
10
|
+
// core/export/transform.ts — "sin parser YAML a proposito, YAGNI, cero deps").
|
|
11
|
+
// Esa decision es sostenible SOLO si algo verifica que la implementacion a
|
|
12
|
+
// mano coincide con la semantica YAML real; si no, cada forma valida que el
|
|
13
|
+
// parser casero no contempla es un bug esperando a un skill que la use. Ya
|
|
14
|
+
// paso dos veces: (1) un block scalar (`description: >-`) se colapsaba a
|
|
15
|
+
// vacio, rompiendo `awm add -a cursor|copilot` y abortando `awm export`
|
|
16
|
+
// entero contra un skill real del registry; (2) este mismo test diferencial
|
|
17
|
+
// descubrio que un escalar single-quoted no deshacia el escape YAML de `'`
|
|
18
|
+
// (duplicado: `''`), devolviendo "it''s" en vez de "it's".
|
|
19
|
+
//
|
|
20
|
+
// js-yaml es devDependency: NO entra en el bundle que instalan los usuarios.
|
|
21
|
+
// La regla de cero dependencias de runtime queda intacta — de hecho este test
|
|
22
|
+
// es lo que la hace defendible.
|
|
23
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
24
|
+
const discovery_1 = require("../../src/core/discovery");
|
|
25
|
+
/** Cada caso es un bloque de frontmatter cuyo `description` debe resolverse
|
|
26
|
+
* IGUAL que como lo hace un parser YAML real. Sin valores esperados
|
|
27
|
+
* hardcodeados a proposito: la referencia es js-yaml, asi que el test no
|
|
28
|
+
* puede "envejecer" hacia una expectativa equivocada. */
|
|
29
|
+
const CASES = [
|
|
30
|
+
['folded (>)', 'description: >\n a\n b'],
|
|
31
|
+
['folded strip (>-)', 'description: >-\n a\n b'],
|
|
32
|
+
['folded keep (>+)', 'description: >+\n a\n b'],
|
|
33
|
+
['literal (|)', 'description: |\n a\n b'],
|
|
34
|
+
['literal strip (|-)', 'description: |-\n a\n b'],
|
|
35
|
+
['folded con parrafos', 'description: >-\n a\n\n b'],
|
|
36
|
+
['literal con parrafos', 'description: |-\n a\n\n b'],
|
|
37
|
+
['bloque seguido de otra clave', 'description: >-\n solo\nname: x'],
|
|
38
|
+
['bloque con indent 4', 'description: >-\n a\n b'],
|
|
39
|
+
['bloque con indent explicito', 'description: >2\n a\n b'],
|
|
40
|
+
['bloque de una sola linea', 'description: >-\n sola'],
|
|
41
|
+
['bloque con colon adentro', 'description: >-\n clave: valor'],
|
|
42
|
+
['bloque con hash adentro', 'description: >-\n a #no-es-comentario'],
|
|
43
|
+
['plain', 'description: hola mundo'],
|
|
44
|
+
['plain con guion', 'description: usa - guiones'],
|
|
45
|
+
['double-quoted con colon', 'description: "hola: mundo"'],
|
|
46
|
+
['double-quoted con comilla escapada', 'description: "dice \\"hola\\""'],
|
|
47
|
+
['double-quoted con \\n', 'description: "a\\nb"'],
|
|
48
|
+
['double-quoted con backslash', 'description: "c:\\\\\\\\ruta"'],
|
|
49
|
+
['single-quoted con apostrofe escapado', "description: 'it''s aqui'"],
|
|
50
|
+
['single-quoted con doble apostrofe', "description: 'a''''b'"],
|
|
51
|
+
['single-quoted con colon', "description: 'clave: valor'"],
|
|
52
|
+
['double-quoted con em-dash', 'description: "em — dash"'],
|
|
53
|
+
// Casos que una revision adversarial encontro y los tests a mano no cubrian.
|
|
54
|
+
['folded con lineas MAS indentadas (no se pliegan)', 'description: >-\n intro\n - item uno\n - item dos\n outro'],
|
|
55
|
+
['folded con indentacion irregular', 'description: >\n foo\n bar\n baz'],
|
|
56
|
+
['indicador con comentario final', 'description: >- # nota al margen\n el texto real'],
|
|
57
|
+
['literal con comentario final', 'description: |- # nota\n linea'],
|
|
58
|
+
['clave description indentada NO gana sobre la real', 'metadata:\n description: nota interna\ndescription: la verdadera'],
|
|
59
|
+
['description dentro del contenido de otro bloque', 'example: |\n description: falsa\ndescription: la verdadera'],
|
|
60
|
+
['plain con comentario final', 'description: hola mundo # un comentario'],
|
|
61
|
+
['plain con hash pegado (NO es comentario)', 'description: usar C# y F#'],
|
|
62
|
+
['plain multilinea (se pliega como folded)', 'description: primera\n segunda\n tercera'],
|
|
63
|
+
['quoted con hash adentro (literal)', 'description: "tiene # adentro"'],
|
|
64
|
+
// Segunda tanda de revision adversarial: espacios, blancos consecutivos y
|
|
65
|
+
// escalares entrecomillados que cierran en una linea posterior.
|
|
66
|
+
['blancos consecutivos en folded', 'description: >-\n a\n\n\n b'],
|
|
67
|
+
['espacios finales dentro de un folded', 'description: >-\n a \n b'],
|
|
68
|
+
['linea solo-espacios dentro de un folded', 'description: >-\n a\n \n b'],
|
|
69
|
+
['double-quoted multilinea', 'description: "hola\n mundo"'],
|
|
70
|
+
['single-quoted multilinea', "description: 'hola\n mundo'"],
|
|
71
|
+
['double-quoted con comentario final', 'description: "x" # comentario'],
|
|
72
|
+
['single-quoted con comentario final', "description: 'y' # comentario"],
|
|
73
|
+
['single-quoted con apostrofe y comentario', "description: 'it''s aqui' # nota"],
|
|
74
|
+
];
|
|
75
|
+
/** Formas que YAML RECHAZA: no hay valor correcto que devolver, y devolver el
|
|
76
|
+
* indicador crudo como si fuera la descripcion seria peor que no devolver
|
|
77
|
+
* nada (se publicaria en el artefacto). El contrato aca es '' — que hace
|
|
78
|
+
* fallar fuerte a parseSkillSource y claudeAiTransform. */
|
|
79
|
+
const INVALID_CASES = [
|
|
80
|
+
['indicador con indent 0', 'description: >0\n a'],
|
|
81
|
+
['indicador con indent de 2 digitos', 'description: >12\n a'],
|
|
82
|
+
['indicador con basura pegada', 'description: >-basura\n a'],
|
|
83
|
+
['indicador con comentario sin espacio', 'description: >-#c\n a'],
|
|
84
|
+
];
|
|
85
|
+
describe('readFrontmatterDescription coincide con un parser YAML real (js-yaml)', () => {
|
|
86
|
+
it.each(CASES)('%s', (_name, frontmatter) => {
|
|
87
|
+
const parsed = js_yaml_1.default.load(frontmatter);
|
|
88
|
+
// Guard del propio fixture: si js-yaml no lo lee como string, el caso
|
|
89
|
+
// esta mal escrito y el test seria vacuo — que falle fuerte.
|
|
90
|
+
expect(typeof parsed.description).toBe('string');
|
|
91
|
+
const expected = parsed.description.trim();
|
|
92
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
|
|
93
|
+
});
|
|
94
|
+
it.each(INVALID_CASES)('%s: YAML lo rechaza, nosotros devolvemos vacio (nunca el indicador crudo)', (_name, frontmatter) => {
|
|
95
|
+
// Guard del fixture: si js-yaml LO ACEPTARA, el caso esta mal clasificado
|
|
96
|
+
// y este test seria vacuo — que falle fuerte.
|
|
97
|
+
expect(() => js_yaml_1.default.load(frontmatter)).toThrow();
|
|
98
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe('');
|
|
99
|
+
});
|
|
100
|
+
it('resuelve el SKILL.md real del registry igual que js-yaml (extract-design-md, el caso que origino el bug)', () => {
|
|
101
|
+
// Forma exacta del skill real de awm-baseline-registry que rompia
|
|
102
|
+
// `awm add -a cursor|copilot` y `awm export frontend`.
|
|
103
|
+
const frontmatter = [
|
|
104
|
+
'name: extract-design-md',
|
|
105
|
+
'version: "1.0.1"',
|
|
106
|
+
'description: >-',
|
|
107
|
+
' Extract a comprehensive design system (DESIGN.md) directly from frontend source',
|
|
108
|
+
' code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.',
|
|
109
|
+
'allowed-tools:',
|
|
110
|
+
' - "Bash"',
|
|
111
|
+
].join('\n');
|
|
112
|
+
const expected = (js_yaml_1.default.load(frontmatter).description).trim();
|
|
113
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
|
|
114
|
+
// Y no absorbio la clave siguiente ni sus items indentados.
|
|
115
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('allowed-tools');
|
|
116
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('Bash');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -46,6 +46,42 @@ it('ignores provider-only mode while retaining canonical instructions', () => {
|
|
|
46
46
|
instructions: expect.stringContaining('You do NOT write code directly.'),
|
|
47
47
|
}); // verifies R8, R9
|
|
48
48
|
});
|
|
49
|
+
it('acepta una description en block scalar y la resuelve (regresion: rompia awm add -a codex)', () => {
|
|
50
|
+
// El parser exigia que TODA linea del frontmatter fuera `clave: valor`, asi
|
|
51
|
+
// que las lineas indentadas de un bloque lanzaban "invalid canonical agent
|
|
52
|
+
// frontmatter line" y abortaban el install de codex. Peor: discovery lee el
|
|
53
|
+
// MISMO archivo y (ya arreglado) mostraba la descripcion bien en el picker,
|
|
54
|
+
// asi que el crash llegaba despues de que la UI dijera que todo estaba OK.
|
|
55
|
+
const source = [
|
|
56
|
+
'---',
|
|
57
|
+
'name: bloque-agente',
|
|
58
|
+
'description: >-',
|
|
59
|
+
' Primera linea de la descripcion',
|
|
60
|
+
' y su continuacion.',
|
|
61
|
+
'---',
|
|
62
|
+
'Cuerpo de instrucciones.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
|
|
65
|
+
name: 'bloque-agente',
|
|
66
|
+
description: 'Primera linea de la descripcion y su continuacion.',
|
|
67
|
+
instructions: 'Cuerpo de instrucciones.',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('acepta un escalar plano multilinea y lo pliega (js-yaml lee exactamente esto)', () => {
|
|
71
|
+
// Ojo: una version previa de este test exigia un throw aca. Estaba mal —
|
|
72
|
+
// `description: x` seguido de una linea indentada es un escalar plano
|
|
73
|
+
// multilinea, YAML perfectamente valido, y js-yaml lo lee como
|
|
74
|
+
// "x suelta e indentada". Asertar el throw codificaba una divergencia con
|
|
75
|
+
// YAML, no un contrato.
|
|
76
|
+
const source = '---\nname: ok\ndescription: x\n suelta e indentada\n---\nbody';
|
|
77
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source).description).toBe('x suelta e indentada');
|
|
78
|
+
});
|
|
79
|
+
it('sigue rechazando una linea que no es ni clave ni continuacion indentada', () => {
|
|
80
|
+
// El fix no afloja la validacion estricta: una linea en columna 0 que no es
|
|
81
|
+
// `clave: valor` sigue siendo frontmatter malformado.
|
|
82
|
+
const source = '---\nname: ok\ndescription: x\nno soy un campo\n---\nbody';
|
|
83
|
+
expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow('invalid canonical agent frontmatter line');
|
|
84
|
+
});
|
|
49
85
|
it.each([
|
|
50
86
|
['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
|
|
51
87
|
['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
|
|
@@ -49,11 +49,18 @@ Body content.
|
|
|
49
49
|
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
50
50
|
expect(rendered).toContain('description: "*starred description"');
|
|
51
51
|
});
|
|
52
|
-
it('
|
|
53
|
-
// Regression
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
52
|
+
it('resuelve un block scalar leyendo sus lineas indentadas, sin emitir el indicador literal', () => {
|
|
53
|
+
// Regression, en dos etapas:
|
|
54
|
+
// 1. La version original tomaba el indicador (`>-`) COMO la descripcion,
|
|
55
|
+
// lo que habria escrito el literal ">-" dentro del .mdc.
|
|
56
|
+
// 2. El remedio siguiente lo detectaba pero lo colapsaba a '' (=> throw),
|
|
57
|
+
// tratando una forma YAML valida como "descripcion ausente" — este
|
|
58
|
+
// test codificaba ESE comportamiento parcial. Rompia `awm add
|
|
59
|
+
// <bundle> -a cursor` contra un skill real del registry baseline
|
|
60
|
+
// (extract-design-md), que usa exactamente esta forma.
|
|
61
|
+
// El contrato correcto, y el que se asserta ahora: leer el texto de las
|
|
62
|
+
// lineas indentadas. La intencion original del test — jamas emitir el
|
|
63
|
+
// indicador crudo — se conserva explicitamente abajo.
|
|
57
64
|
const source = `---
|
|
58
65
|
name: block-skill
|
|
59
66
|
description: >-
|
|
@@ -63,16 +70,27 @@ description: >-
|
|
|
63
70
|
|
|
64
71
|
Body content.
|
|
65
72
|
`;
|
|
66
|
-
|
|
73
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
74
|
+
expect(rendered).toContain('description: This description spans multiple lines.');
|
|
75
|
+
expect(rendered).not.toContain('>-');
|
|
67
76
|
});
|
|
68
77
|
it('quotes a description containing a mid-string " #" (starts a YAML comment, truncating the rest)', () => {
|
|
69
78
|
// Regression: the original YAML_UNSAFE regex only caught `#` at the START
|
|
70
79
|
// of the string — a `#` preceded by whitespace ANYWHERE in a plain scalar
|
|
71
80
|
// also starts a comment. Unquoted, "Use this #important skill" would
|
|
72
81
|
// render as YAML that silently truncates to "Use this".
|
|
82
|
+
//
|
|
83
|
+
// El fuente esta ENTRECOMILLADO a proposito: ahi el `#` es literal y
|
|
84
|
+
// sobrevive al parseo, que es la unica manera de que un `#` llegue al
|
|
85
|
+
// renderer y haya algo que escapar. Antes el fixture usaba la forma SIN
|
|
86
|
+
// comillas, pero YAML dice que ahi ` #` abre un comentario — el lector lo
|
|
87
|
+
// trataba como texto (divergencia con cualquier parser real) y este test
|
|
88
|
+
// se apoyaba en esa divergencia. El contrato que el test realmente
|
|
89
|
+
// protege — nunca emitir un `#` sin comillas en la SALIDA — queda intacto
|
|
90
|
+
// y ahora se ejercita con una entrada que de verdad lo contiene.
|
|
73
91
|
const source = `---
|
|
74
92
|
name: hash-skill
|
|
75
|
-
description: Use this #important skill
|
|
93
|
+
description: "Use this #important skill"
|
|
76
94
|
---
|
|
77
95
|
|
|
78
96
|
Body content.
|
|
@@ -81,6 +99,18 @@ Body content.
|
|
|
81
99
|
expect(rendered).toContain('description: "Use this #important skill"');
|
|
82
100
|
expect(rendered).not.toContain('description: Use this #important skill');
|
|
83
101
|
});
|
|
102
|
+
it('trata un " #" en una description SIN comillas como comentario YAML, igual que un parser real', () => {
|
|
103
|
+
const source = `---
|
|
104
|
+
name: hash-plano
|
|
105
|
+
description: Use this #important skill
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Body content.
|
|
109
|
+
`;
|
|
110
|
+
// js-yaml devuelve "Use this" para esta entrada: el comentario no es parte
|
|
111
|
+
// del valor. El renderer debe coincidir con esa lectura, no inventar texto.
|
|
112
|
+
expect((0, cursor_mdc_1.renderCursorMdc)(source)).toContain('description: Use this\n');
|
|
113
|
+
});
|
|
84
114
|
it('quotes a description containing an embedded null byte / control character instead of emitting it raw', () => {
|
|
85
115
|
// Regression: an embedded control/null byte is invalid in a YAML plain
|
|
86
116
|
// scalar regardless of position — the original code's YAML_UNSAFE regex
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Regresion: `description: >-` (YAML block scalar) rompia `awm add <bundle>
|
|
4
|
+
// -a copilot|cursor` contra un skill REAL del registry baseline
|
|
5
|
+
// (skills/extract-design-md/SKILL.md, que usa exactamente esta forma).
|
|
6
|
+
//
|
|
7
|
+
// Un block scalar es YAML perfectamente valido: el indicador (`>-`, `|`, ...)
|
|
8
|
+
// va en la linea de la clave y el texto real vive en las lineas indentadas
|
|
9
|
+
// que siguen. El parser trataba el indicador como si fuera el valor, lo
|
|
10
|
+
// detectaba como "no es una descripcion de verdad", y lo colapsaba a '' —
|
|
11
|
+
// que en `parseSkillSource` significa throw ("requires a non-empty
|
|
12
|
+
// description") y en `readArtifactDescription` significa degradar en
|
|
13
|
+
// silencio a descripcion vacia. Ninguno de los dos leia jamas las lineas
|
|
14
|
+
// siguientes, que es donde estaba el texto todo el tiempo.
|
|
15
|
+
const skill_source_1 = require("../../../src/core/renderers/skill-source");
|
|
16
|
+
const cursor_mdc_1 = require("../../../src/core/renderers/cursor-mdc");
|
|
17
|
+
const copilot_instructions_1 = require("../../../src/core/renderers/copilot-instructions");
|
|
18
|
+
// Copia fiel del frontmatter de skills/extract-design-md/SKILL.md en
|
|
19
|
+
// awm-baseline-registry — el caso real que dispara el bug, no un fixture
|
|
20
|
+
// inventado.
|
|
21
|
+
const realBlockScalarSkill = `---
|
|
22
|
+
name: extract-design-md
|
|
23
|
+
version: "1.0.1"
|
|
24
|
+
description: >-
|
|
25
|
+
Extract a comprehensive design system (DESIGN.md) directly from frontend source
|
|
26
|
+
code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Extract Design MD
|
|
30
|
+
|
|
31
|
+
Body content.
|
|
32
|
+
`;
|
|
33
|
+
describe('parseSkillSource: descripciones en block scalar (regresion real del registry)', () => {
|
|
34
|
+
it('lee el texto de las lineas indentadas de un `>-`, en vez de tratar el indicador como el valor', () => {
|
|
35
|
+
const { description } = (0, skill_source_1.parseSkillSource)(realBlockScalarSkill);
|
|
36
|
+
expect(description).toBe('Extract a comprehensive design system (DESIGN.md) directly from frontend source ' +
|
|
37
|
+
'code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.');
|
|
38
|
+
});
|
|
39
|
+
it('pliega (folded, `>`) las lineas en espacios — no conserva los saltos de linea del fuente', () => {
|
|
40
|
+
const source = `---
|
|
41
|
+
name: folded
|
|
42
|
+
description: >
|
|
43
|
+
primera linea
|
|
44
|
+
segunda linea
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Body.
|
|
48
|
+
`;
|
|
49
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea segunda linea');
|
|
50
|
+
});
|
|
51
|
+
it('conserva los saltos de linea de un literal (`|`) — semantica YAML distinta de `>`', () => {
|
|
52
|
+
const source = `---
|
|
53
|
+
name: literal
|
|
54
|
+
description: |
|
|
55
|
+
primera linea
|
|
56
|
+
segunda linea
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Body.
|
|
60
|
+
`;
|
|
61
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea\nsegunda linea');
|
|
62
|
+
});
|
|
63
|
+
it('trata una linea en blanco dentro de un folded como separador de parrafo, no como espacio', () => {
|
|
64
|
+
const source = `---
|
|
65
|
+
name: folded-parrafos
|
|
66
|
+
description: >-
|
|
67
|
+
parrafo uno
|
|
68
|
+
|
|
69
|
+
parrafo dos
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
Body.
|
|
73
|
+
`;
|
|
74
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('parrafo uno\nparrafo dos');
|
|
75
|
+
});
|
|
76
|
+
it('no absorbe las claves siguientes del frontmatter como parte del bloque (corta en la primera linea no indentada)', () => {
|
|
77
|
+
const source = `---
|
|
78
|
+
description: >-
|
|
79
|
+
solo esto pertenece al bloque
|
|
80
|
+
name: no-soy-parte-del-bloque
|
|
81
|
+
version: "9.9.9"
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
Body.
|
|
85
|
+
`;
|
|
86
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('solo esto pertenece al bloque');
|
|
87
|
+
});
|
|
88
|
+
it('resuelve el bloque igual con line endings CRLF (el repo es CRLF-tolerante y corre CI en windows-latest)', () => {
|
|
89
|
+
const source = '---\r\nname: crlf\r\ndescription: >-\r\n linea uno\r\n linea dos\r\n---\r\n\r\nBody aqui.\r\n';
|
|
90
|
+
expect((0, skill_source_1.parseSkillSource)(source).description).toBe('linea uno linea dos');
|
|
91
|
+
});
|
|
92
|
+
it('sigue lanzando si el bloque esta genuinamente vacio — un indicador sin lineas indentadas NO es una descripcion', () => {
|
|
93
|
+
const source = `---
|
|
94
|
+
name: bloque-vacio
|
|
95
|
+
description: >-
|
|
96
|
+
name: otra-clave
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
Body.
|
|
100
|
+
`;
|
|
101
|
+
expect(() => (0, skill_source_1.parseSkillSource)(source)).toThrow(/non-empty description/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('renderers: el skill real del registry se renderiza para ambos providers', () => {
|
|
105
|
+
it('renderCursorMdc emite la descripcion real, nunca el indicador `>-` literal', () => {
|
|
106
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(realBlockScalarSkill);
|
|
107
|
+
expect(rendered).toContain('Extract a comprehensive design system');
|
|
108
|
+
// El modo de fallo que este test ancla: emitir el indicador crudo.
|
|
109
|
+
expect(rendered).not.toContain('description: >-');
|
|
110
|
+
expect(rendered).not.toContain('description: ""');
|
|
111
|
+
});
|
|
112
|
+
it('renderCopilotInstructions no crashea (no necesita la descripcion, pero parseSkillSource la exigia igual)', () => {
|
|
113
|
+
const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(realBlockScalarSkill);
|
|
114
|
+
expect(rendered).toContain('applyTo: "**"');
|
|
115
|
+
expect(rendered).toContain('# Extract Design MD');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -44,6 +44,15 @@ describe('truncate', () => {
|
|
|
44
44
|
it('returns empty for non-positive width', () => {
|
|
45
45
|
expect((0, text_1.truncate)('abc', 0)).toBe('');
|
|
46
46
|
});
|
|
47
|
+
it('colapsa saltos de linea a un espacio — la celda es de UNA linea', () => {
|
|
48
|
+
// Regresion: `displayWidth` cuenta `\n` como ancho 1, asi que una string
|
|
49
|
+
// multilinea corta pasaba el chequeo y se emitia cruda, partiendo la fila
|
|
50
|
+
// de `awm list` y desalineando todo lo siguiente. Alcanzable desde que el
|
|
51
|
+
// lector de frontmatter resuelve block scalars literales (`|`).
|
|
52
|
+
expect((0, text_1.truncate)('linea uno\nlinea dos', 80)).toBe('linea uno linea dos');
|
|
53
|
+
expect((0, text_1.truncate)('a\r\n b', 80)).toBe('a b');
|
|
54
|
+
expect((0, text_1.truncate)('linea uno\nlinea dos', 12)).toBe('linea uno l…');
|
|
55
|
+
});
|
|
47
56
|
});
|
|
48
57
|
describe('wrap', () => {
|
|
49
58
|
it('breaks text at word boundaries within width', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.5",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/jest": "^30.0.0",
|
|
46
|
+
"@types/js-yaml": "4.0.9",
|
|
46
47
|
"@types/node": "^25.3.0",
|
|
47
48
|
"dependency-cruiser": "^17.4.3",
|
|
48
49
|
"eslint": "^10.4.1",
|
|
49
50
|
"jest": "^30.2.0",
|
|
51
|
+
"js-yaml": "4.1.0",
|
|
50
52
|
"ts-jest": "^29.4.6",
|
|
51
53
|
"typescript": "^5.9.3"
|
|
52
54
|
}
|