agentic-workflow-manager 3.13.4 → 3.13.6

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.
Files changed (90) hide show
  1. package/dist/src/commands/add.js +5 -0
  2. package/dist/src/commands/doctor.js +1 -0
  3. package/dist/src/commands/hooks/claude.js +15 -2
  4. package/dist/src/commands/hooks/shared.js +6 -1
  5. package/dist/src/commands/init.js +1 -0
  6. package/dist/src/commands/pin.js +24 -4
  7. package/dist/src/commands/preflight/checks.js +3 -2
  8. package/dist/src/commands/registry/add.js +1 -1
  9. package/dist/src/commands/sensors/index.js +7 -0
  10. package/dist/src/commands/sensors/init.js +48 -7
  11. package/dist/src/commands/sensors/install.js +20 -11
  12. package/dist/src/commands/sensors/run.js +31 -29
  13. package/dist/src/commands/sync.js +29 -0
  14. package/dist/src/commands/watch/index.js +11 -1
  15. package/dist/src/core/artifact-name.js +80 -0
  16. package/dist/src/core/bundle-install.js +55 -12
  17. package/dist/src/core/bundles.js +72 -13
  18. package/dist/src/core/context/regenerate.js +21 -4
  19. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  20. package/dist/src/core/diagnostics/checks.js +26 -2
  21. package/dist/src/core/diagnostics/context.js +91 -25
  22. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  23. package/dist/src/core/discovery.js +15 -23
  24. package/dist/src/core/export/transform.js +30 -25
  25. package/dist/src/core/frontmatter.js +223 -0
  26. package/dist/src/core/init/mutation-targets.js +11 -0
  27. package/dist/src/core/init/steps.js +70 -19
  28. package/dist/src/core/install-planner.js +62 -17
  29. package/dist/src/core/install-transaction.js +13 -1
  30. package/dist/src/core/journal/adapter.js +10 -1
  31. package/dist/src/core/paths.js +61 -8
  32. package/dist/src/core/profile.js +12 -0
  33. package/dist/src/core/provider-artifacts.js +38 -13
  34. package/dist/src/core/provider-version.js +12 -6
  35. package/dist/src/core/registries.js +40 -19
  36. package/dist/src/core/renderers/canonical-agent.js +33 -2
  37. package/dist/src/core/renderers/registry.js +37 -0
  38. package/dist/src/core/renderers/skill-source.js +11 -19
  39. package/dist/src/core/skill-integrity.js +71 -10
  40. package/dist/src/core/update-check.js +14 -2
  41. package/dist/src/core/versioning.js +17 -2
  42. package/dist/src/index.js +31 -23
  43. package/dist/src/providers/index.js +7 -2
  44. package/dist/src/ui/text.js +10 -2
  45. package/dist/src/utils/config.js +7 -8
  46. package/dist/tests/commands/doctor.test.js +6 -0
  47. package/dist/tests/commands/pin.test.js +29 -2
  48. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  49. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  50. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  51. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  52. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  53. package/dist/tests/commands/sensors/run.test.js +24 -51
  54. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  55. package/dist/tests/commands/sensors/status.test.js +27 -12
  56. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  57. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  58. package/dist/tests/core/context/regenerate.test.js +39 -8
  59. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  60. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  61. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  62. package/dist/tests/core/discovery.test.js +10 -1
  63. package/dist/tests/core/export/transform.test.js +110 -21
  64. package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
  65. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  66. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  67. package/dist/tests/core/init/orchestrator.test.js +1 -1
  68. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  69. package/dist/tests/core/init/steps.test.js +11 -5
  70. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  71. package/dist/tests/core/paths.test.js +7 -34
  72. package/dist/tests/core/project-skill-links.test.js +159 -0
  73. package/dist/tests/core/provider-artifacts.test.js +66 -14
  74. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  75. package/dist/tests/core/reconciliation.test.js +15 -15
  76. package/dist/tests/core/registry-manifest.test.js +1 -1
  77. package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
  78. package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
  79. package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
  80. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  81. package/dist/tests/core/skill-integrity.test.js +10 -10
  82. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  83. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  84. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  85. package/dist/tests/providers/index.test.js +1 -1
  86. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  87. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  88. package/dist/tests/ui/text.test.js +9 -0
  89. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  90. package/package.json +6 -1
@@ -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
- for (const line of match[1].split(/\r?\n/)) {
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
- const description = fields.get('description') ?? '';
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');
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderedFilename = renderedFilename;
4
+ exports.rendererExtension = rendererExtension;
5
+ /** Extension que el renderer estampa sobre el nombre base, o `null` cuando el
6
+ * artefacto se instala con el nombre tal cual (renderer `link`). */
7
+ const RENDERER_EXTENSION = {
8
+ link: null,
9
+ 'codex-agent-toml': '.toml',
10
+ 'cursor-mdc': '.mdc',
11
+ 'copilot-instructions': '.instructions.md',
12
+ };
13
+ /** `.md` es la unica extension que un `installName` de artefacto puede traer.
14
+ * Deliberadamente NO se usa `path.parse().name`: eso corta desde el ULTIMO
15
+ * punto, asi que un skill llamado `v1.2-migration` se truncaria a `v1`,
16
+ * perdiendo `2-migration` y arriesgando colision con otro skill `v1`. */
17
+ function stripMarkdownSuffix(installName) {
18
+ return installName.endsWith('.md') ? installName.slice(0, -'.md'.length) : installName;
19
+ }
20
+ /**
21
+ * Nombre de archivo FISICO que `renderer` produce para `installName`.
22
+ *
23
+ * Es la funcion que deben usar por igual el escritor (al planificar la
24
+ * instalacion) y todo lector (al preguntar "¿esta instalado?"). Que ambos lados
25
+ * salgan de aca es lo que hace estructuralmente imposible que vuelvan a
26
+ * discrepar.
27
+ */
28
+ function renderedFilename(installName, renderer) {
29
+ const extension = RENDERER_EXTENSION[renderer];
30
+ return extension === null ? installName : `${stripMarkdownSuffix(installName)}${extension}`;
31
+ }
32
+ /** Extension del renderer, o `null` si instala con el nombre tal cual. Para los
33
+ * pocos casos que necesitan la extension suelta (p. ej. "¿hay ALGUN archivo
34
+ * renderizado en este directorio?") en vez de un nombre concreto. */
35
+ function rendererExtension(renderer) {
36
+ return RENDERER_EXTENSION[renderer];
37
+ }
@@ -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 discovery_1 = require("../discovery");
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, discovery_1.matchFrontmatterBlock)(source);
23
+ const frontmatter = (0, frontmatter_1.matchFrontmatterBlock)(source);
24
24
  if (frontmatter === null)
25
25
  throw new Error('skill source requires YAML frontmatter');
26
- const line = frontmatter.split(/\r?\n/).find((l) => /^description\s*:/.test(l));
27
- if (!line)
28
- throw new Error('skill source requires a non-empty description');
29
- let description = line.replace(/^description\s*:/, '').trim();
30
- if ((description.startsWith('"') && description.endsWith('"')) ||
31
- (description.startsWith("'") && description.endsWith("'"))) {
32
- description = description.slice(1, -1);
33
- }
34
- // A YAML block scalar indicator (`>-`, `|-`, `>`, `|`, `>+`, `|+`) means the
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]*)$/);
@@ -3,12 +3,14 @@ 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.classifyGlobalSkills = classifyGlobalSkills;
7
- exports.repairGlobalSkills = repairGlobalSkills;
6
+ exports.classifySkillLinks = classifySkillLinks;
7
+ exports.repairSkillLinks = repairSkillLinks;
8
8
  exports.reconcileAllSkillLinks = reconcileAllSkillLinks;
9
+ exports.reconcileProjectSkillLinks = reconcileProjectSkillLinks;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const providers_1 = require("../providers");
13
+ const paths_1 = require("./paths");
12
14
  function findRegistrySkillPath(registryContentDirs, name) {
13
15
  for (const root of registryContentDirs) {
14
16
  const p = path_1.default.join(root, 'skills', name);
@@ -17,8 +19,11 @@ function findRegistrySkillPath(registryContentDirs, name) {
17
19
  }
18
20
  return null;
19
21
  }
20
- /** Clasifica cada entrada de `skillsDir` (read-only, no muta nada). */
21
- function classifyGlobalSkills(skillsDir, registryContentDirs) {
22
+ /** Clasifica cada entrada de `skillsDir` (read-only, no muta nada).
23
+ * Agnostico al alcance: `skillsDir` es global (`provider.skill.global`) o de
24
+ * proyecto (`<projectRoot>/<provider.skill.local>`) — el nombre decia "Global" y
25
+ * eso basto para que nadie lo apuntara nunca a un dir de proyecto. */
26
+ function classifySkillLinks(skillsDir, registryContentDirs) {
22
27
  const out = { valid: [], repairable: [], dead: [] };
23
28
  let entries;
24
29
  try {
@@ -51,10 +56,11 @@ function classifyGlobalSkills(skillsDir, registryContentDirs) {
51
56
  return out;
52
57
  }
53
58
  /** Re-linkea los repairable al primer root que tenga la skill y poda los dead. Idempotente. Cada
54
- * symlink aislado en try/catch — una falla no aborta el resto. */
55
- function repairGlobalSkills(skillsDir, registryContentDirs) {
59
+ * symlink aislado en try/catch — una falla no aborta el resto. Agnostico al alcance:
60
+ * ver la nota de `classifySkillLinks`. */
61
+ function repairSkillLinks(skillsDir, registryContentDirs) {
56
62
  const result = { relinked: [], pruned: [], failed: [] };
57
- const { repairable, dead } = classifyGlobalSkills(skillsDir, registryContentDirs);
63
+ const { repairable, dead } = classifySkillLinks(skillsDir, registryContentDirs);
58
64
  for (const name of repairable) {
59
65
  const p = path_1.default.join(skillsDir, name);
60
66
  try {
@@ -63,8 +69,35 @@ function repairGlobalSkills(skillsDir, registryContentDirs) {
63
69
  result.failed.push(name);
64
70
  continue;
65
71
  }
66
- fs_1.default.rmSync(p, { force: true });
67
- fs_1.default.symlinkSync(target, p, 'dir');
72
+ // Dos correcciones, ambas criticas en Windows:
73
+ //
74
+ // 1. `'junction'` en win32, no `'dir'`. Un symlink de directorio
75
+ // exige SeCreateSymbolicLinkPrivilege, denegado por defecto en
76
+ // cuentas sin privilegios (incluido el runner windows-latest);
77
+ // una junction la puede crear cualquiera y libuv la reporta
78
+ // igual. `executor.ts` ya documenta esto en extenso — este sitio
79
+ // nunca recibio el fix.
80
+ //
81
+ // 2. Se crea el link NUEVO antes de retirar el viejo. Antes el
82
+ // `rmSync` iba primero, asi que en Windows el link se borraba y
83
+ // la recreacion fallaba: el usuario quedaba PEOR que antes, y
84
+ // con la entrada ya ausente el proximo `awm init` ni siquiera
85
+ // podia verla como reparable. Un fallo ahora deja el estado
86
+ // original intacto.
87
+ const staged = path_1.default.join(skillsDir, `.${name}.${process.pid}.relink`);
88
+ try {
89
+ fs_1.default.rmSync(staged, { recursive: true, force: true });
90
+ fs_1.default.symlinkSync(target, staged, (0, paths_1.isWindowsNative)() ? 'junction' : 'dir');
91
+ fs_1.default.rmSync(p, { recursive: true, force: true });
92
+ fs_1.default.renameSync(staged, p);
93
+ }
94
+ finally {
95
+ // El staging jamas debe sobrevivir a esta iteracion: si quedara,
96
+ // `classifySkillLinks` lo veria como un symlink valido (apunta
97
+ // al registry, que existe) y nunca lo clasificaria como muerto,
98
+ // asi que se quedaria en el directorio para siempre.
99
+ fs_1.default.rmSync(staged, { recursive: true, force: true });
100
+ }
68
101
  result.relinked.push(name);
69
102
  }
70
103
  catch {
@@ -92,7 +125,35 @@ function reconcileAllSkillLinks(registryContentDirs) {
92
125
  const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
93
126
  if (skillsDir === null || !fs_1.default.existsSync(skillsDir))
94
127
  continue;
95
- out.push({ agent, result: repairGlobalSkills(skillsDir, registryContentDirs) });
128
+ out.push({ agent, result: repairSkillLinks(skillsDir, registryContentDirs) });
129
+ }
130
+ return out;
131
+ }
132
+ /**
133
+ * El equivalente de `reconcileAllSkillLinks` para el alcance de PROYECTO: reconcilia
134
+ * los symlinks de skills bajo `<projectRoot>/<provider.skill.local>` de cada agente
135
+ * pedido. Es la mitad que faltaba — todo lo de arriba servia igual para un dir local,
136
+ * pero ningun llamador lo apuntaba ahi, asi que un link colgante de proyecto no se
137
+ * curaba, no se podaba y no se reportaba en ninguna superficie. Para Copilot
138
+ * (`skill.global === null`) esto no era una mitad sino el total: TODAS sus skills son
139
+ * de proyecto.
140
+ *
141
+ * Un dir inexistente se omite (no hay nada que reconciliar y crearlo seria instalar,
142
+ * no reparar). Solo toca symlinks colgantes: los archivos y directorios reales del
143
+ * usuario, y los links vivos, quedan intactos — ver `classifySkillLinks`.
144
+ */
145
+ function reconcileProjectSkillLinks(projectRoot, agents, registryContentDirs) {
146
+ const out = [];
147
+ const seen = new Set();
148
+ for (const agent of agents) {
149
+ // Varios providers comparten dir local (opencode y codex → `.agents/skills`).
150
+ // Reconciliar dos veces el mismo dir reportaria las mismas curaciones por
151
+ // duplicado, y la segunda pasada las veria ya sanas: se hace una sola vez.
152
+ const skillsDir = path_1.default.join(projectRoot, (0, providers_1.providerFor)(agent).skill.local);
153
+ if (seen.has(skillsDir) || !fs_1.default.existsSync(skillsDir))
154
+ continue;
155
+ seen.add(skillsDir);
156
+ out.push({ agent, result: repairSkillLinks(skillsDir, registryContentDirs) });
96
157
  }
97
158
  return out;
98
159
  }
@@ -21,6 +21,18 @@ const picocolors_1 = __importDefault(require("picocolors"));
21
21
  const prompts_1 = require("@clack/prompts");
22
22
  const cli_version_1 = require("./cli-version");
23
23
  const versioning_1 = require("./versioning");
24
+ /** El aviso de actualizacion es cosmetico: una version ilegible (de la cache o
25
+ * del registro de npm) no debe romper el comando que el usuario pidio — solo
26
+ * se omite el aviso. El gate de `minCliVersion`, en cambio, falla CERRADO
27
+ * (ver verifyMinCliVersions). */
28
+ function isNewer(a, b) {
29
+ try {
30
+ return (0, versioning_1.compareSemver)(a, b) > 0;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
24
36
  const paths_1 = require("./paths");
25
37
  const TTL_MS = 24 * 60 * 60 * 1000;
26
38
  const REGISTRY_URL = `https://registry.npmjs.org/${cli_version_1.CLI_PACKAGE_NAME}/latest`;
@@ -70,7 +82,7 @@ function maybeNotifyUpdate(opts) {
70
82
  const now = opts?.now ?? Date.now();
71
83
  const spawnWorker = opts?.spawnWorker ?? spawnRefreshWorker;
72
84
  const cache = readUpdateCache();
73
- if (cache?.latest && (0, versioning_1.compareSemver)(cache.latest, (0, cli_version_1.cliVersion)()) > 0) {
85
+ if (cache?.latest && isNewer(cache.latest, (0, cli_version_1.cliVersion)())) {
74
86
  console.log(picocolors_1.default.dim(`\n⬆ awm v${cache.latest} available → npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
75
87
  }
76
88
  if (!cache || now - cache.lastCheck > TTL_MS)
@@ -83,7 +95,7 @@ async function offerSelfUpdate(deps = {}) {
83
95
  const current = deps.current ?? (0, cli_version_1.cliVersion)();
84
96
  const latest = deps.latest !== undefined ? deps.latest : await fetchLatestVersion(deps.fetchImpl ?? fetch);
85
97
  writeUpdateCache({ lastCheck: Date.now(), latest: latest ?? null });
86
- if (!latest || (0, versioning_1.compareSemver)(latest, current) <= 0)
98
+ if (!latest || !isNewer(latest, current))
87
99
  return;
88
100
  const confirmImpl = deps.confirmImpl ?? (async (message) => {
89
101
  const r = await (0, prompts_1.confirm)({ message });
@@ -105,8 +105,23 @@ function machineVersionOpts(registryName) {
105
105
  }
106
106
  }
107
107
  /** Compara "X.Y.Z" vs "X.Y.Z" (sin prefijo v) numéricamente. <0, 0, >0. */
108
+ /** Compara dos semver X.Y.Z.
109
+ *
110
+ * Antes devolvia `NaN` ante una entrada malformada (`'1.2'`, `'x.y.z'`), y como
111
+ * `NaN > 0` y `NaN < 0` son AMBOS false, el gate de version fallaba ABIERTO:
112
+ * `verifyMinCliVersions` dejaba pasar en silencio un CLI por debajo del minimo,
113
+ * y el aviso de actualizacion no disparaba nunca. Para un gate, la direccion
114
+ * correcta de fallo es la contraria. Ahora una entrada que no es semver lanza,
115
+ * nombrando el valor: es el llamador quien decide como tratarlo, con
116
+ * informacion suficiente para hacerlo. */
108
117
  function compareSemver(a, b) {
109
- const pa = a.split('.').map(Number);
110
- const pb = b.split('.').map(Number);
118
+ const parse = (v) => {
119
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v).trim());
120
+ if (!m)
121
+ throw new Error(`not a semver version: ${JSON.stringify(v)}`);
122
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
123
+ };
124
+ const pa = parse(a);
125
+ const pb = parse(b);
111
126
  return (pa[0] - pb[0]) || (pa[1] - pb[1]) || (pa[2] - pb[2]);
112
127
  }
package/dist/src/index.js CHANGED
@@ -18,6 +18,9 @@ const provider_preflight_1 = require("./ui/provider-preflight");
18
18
  const discovery_1 = require("./core/discovery");
19
19
  const bundles_1 = require("./core/bundles");
20
20
  const registries_1 = require("./core/registries");
21
+ const install_planner_1 = require("./core/install-planner");
22
+ const install_transaction_1 = require("./core/install-transaction");
23
+ const profile_1 = require("./core/profile");
21
24
  const path_1 = __importDefault(require("path"));
22
25
  const picocolors_1 = __importDefault(require("picocolors"));
23
26
  const fs_1 = __importDefault(require("fs"));
@@ -166,31 +169,33 @@ program.command('add [name]')
166
169
  (0, prompts_1.outro)(picocolors_1.default.yellow('No artifacts available.'));
167
170
  return;
168
171
  }
169
- const preflightOk = (0, provider_preflight_1.preflightLinkArtifactsForCli)(targetAgents.flatMap((agent) => artifactsToInstall.map((artifact) => ({ agent, artifact }))));
170
- if (!preflightOk)
171
- return;
172
172
  const installSpinner = (0, prompts_1.spinner)();
173
173
  installSpinner.start('Installing artifacts...');
174
174
  try {
175
- const installed = [];
176
- const skipped = [];
177
- for (const currentAgent of targetAgents) {
178
- for (const artifact of artifactsToInstall) {
179
- const config = (0, providers_1.assertLinkRenderer)(artifact.type, currentAgent);
180
- if (config === null) {
181
- skipped.push(`${artifact.name} (${currentAgent})`);
182
- continue;
183
- }
184
- const targetDir = config[scopeVal];
185
- if (targetDir === null) {
186
- skipped.push(`${artifact.name} (${currentAgent})`);
187
- continue;
188
- }
189
- const finalDest = path_1.default.join(targetDir, artifact.name);
190
- (0, executor_1.installArtifact)(artifact.sourcePath, finalDest, methodVal);
191
- installed.push(`${artifact.name} → ${currentAgent} (${scopeVal})`);
192
- }
193
- }
175
+ // Se delega en el MISMO pipeline que `awm add <bundle>`
176
+ // (planInstall + applyInstallPlan): transaccional, con backups,
177
+ // rollback y soporte real de renderers.
178
+ //
179
+ // Antes esto instalaba a mano y pasaba por `assertLinkRenderer`,
180
+ // que TIRA para cualquier renderer que no sea `link` — asi que
181
+ // `awm add --all` moria con "Renderer 'cursor-mdc' … is not
182
+ // implemented yet" para cursor y copilot, y para CODEX era puro
183
+ // dano colateral: sus skills son `link` e instalan bien, pero un
184
+ // solo artefacto `agent` mataba la corrida entera. El pipeline
185
+ // real ya soporta los tres renderers — `awm add <bundle> -a cursor`
186
+ // lo demuestra — asi que el unico problema era este camino paralelo.
187
+ const summary = (0, install_transaction_1.applyInstallPlan)((0, install_planner_1.planInstall)({
188
+ artifacts: artifactsToInstall.map((a) => ({
189
+ name: a.name, type: a.type, installName: a.name, sourcePath: a.sourcePath,
190
+ })),
191
+ selectedAgents: targetAgents,
192
+ enabledAgents: prefs.enabledAgents,
193
+ scope: scopeVal,
194
+ projectRoot: (0, profile_1.findProjectRoot)(process.cwd()) ?? process.cwd(),
195
+ method: methodVal,
196
+ }));
197
+ const installed = summary.installed;
198
+ const skipped = summary.skipped;
194
199
  const updatedPrefs = targetAgents.reduce((current, agent) => (0, config_1.enableAgent)(current, agent), prefs);
195
200
  (0, config_1.savePreferences)({
196
201
  ...updatedPrefs,
@@ -497,7 +502,10 @@ program.command('remove')
497
502
  });
498
503
  handleCancel(scopeChoice);
499
504
  const scopeVal = scopeChoice;
500
- const installed = (0, provider_artifacts_1.scanLegacyArtifacts)(targetAgents, scopeVal);
505
+ // projectRoot explicito: `config.local` es relativo, y resolverlo contra
506
+ // `process.cwd()` hacia que `awm remove` no encontrara nada desde un
507
+ // subdirectorio — y que le pasara rutas RELATIVAS a fs.rmSync cuando si.
508
+ const installed = (0, provider_artifacts_1.scanLegacyArtifacts)(targetAgents, scopeVal, (0, profile_1.findProjectRoot)(process.cwd()) ?? process.cwd());
501
509
  if (installed.length === 0) {
502
510
  (0, prompts_1.outro)(picocolors_1.default.yellow('No installed artifacts found for the selected agents/scope.'));
503
511
  process.exit(0);
@@ -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.UnsupportedRendererError = exports.AGENT_TARGETS = void 0;
6
+ exports.UnsupportedRendererError = exports.RENDERER_IDS = exports.AGENT_TARGETS = void 0;
7
7
  exports.isAgentTarget = isAgentTarget;
8
8
  exports.requireAgentTarget = requireAgentTarget;
9
9
  exports.unsupportedScopeError = unsupportedScopeError;
@@ -18,6 +18,11 @@ exports.getInjection = getInjection;
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const paths_1 = require("../core/paths");
20
20
  exports.AGENT_TARGETS = ['antigravity', 'opencode', 'claude-code', 'codex', 'cursor', 'copilot'];
21
+ /** Runtime-enumerable, so the renderer table in `core/renderers/registry.ts` can be
22
+ * checked for completeness against it (tests/structural/renderer-table-is-single-source).
23
+ * A type-only union cannot be iterated, which is part of why four partial copies of the
24
+ * renderer→extension mapping could drift apart unnoticed. */
25
+ exports.RENDERER_IDS = ['link', 'codex-agent-toml', 'cursor-mdc', 'copilot-instructions'];
21
26
  function isAgentTarget(value) {
22
27
  return typeof value === 'string' &&
23
28
  exports.AGENT_TARGETS.includes(value);
@@ -102,7 +107,7 @@ function providers() {
102
107
  codex: {
103
108
  label: 'Codex',
104
109
  minimumVersion: '0.145.0',
105
- versionCommand: { command: 'codex', args: ['--version'] },
110
+ versionCommand: { command: 'codex', args: ['--version'], versionPattern: /^codex-cli (\d+\.\d+\.\d+)$/ },
106
111
  skill: {
107
112
  global: path_1.default.join(home, '.agents/skills'),
108
113
  local: '.agents/skills',
@@ -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)
@@ -13,6 +13,7 @@ const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const providers_1 = require("../providers");
15
15
  const paths_1 = require("../core/paths");
16
+ const atomic_file_1 = require("../core/atomic-file");
16
17
  const DEFAULT_PREFS = {
17
18
  // claude-code matches `awm init`'s own documented default (see init.ts / `awm init --help`).
18
19
  // Previously 'antigravity', which silently mis-installed bundles in claude-code
@@ -118,14 +119,12 @@ function loadPreferences(initialAgent = 'claude-code') {
118
119
  }
119
120
  function savePreferences(prefs) {
120
121
  const normalized = normalizePreferences(prefs, false).prefs;
121
- const file = prefsFile();
122
- fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
123
- const temp = `${file}.${process.pid}.tmp`;
124
- fs_1.default.writeFileSync(temp, JSON.stringify(normalized, null, 2) + '\n', {
125
- encoding: 'utf8',
126
- mode: 0o600,
127
- });
128
- fs_1.default.renameSync(temp, file);
122
+ // `writeFileAtomic`, not a second hand-rolled write-temp-then-rename. The local copy
123
+ // predated the primitive and had drifted below it: no fsync, a pid-only temp name,
124
+ // and none of the guards the shared one grew. Same 0o600 intent and same
125
+ // mode-preservation semantics as `artifact-state.ts`, the other 0o600 file written
126
+ // through it — a file of this sensitivity has an established treatment here.
127
+ (0, atomic_file_1.writeFileAtomic)(prefsFile(), JSON.stringify(normalized, null, 2) + '\n', 0o600);
129
128
  }
130
129
  function enableAgent(prefs, agent) {
131
130
  normalizePreferences(prefs);
@@ -148,6 +148,12 @@ describe('runDoctor', () => {
148
148
  expect(code).toBe(1);
149
149
  });
150
150
  it('reports every enabled provider and stable remediation codes in JSON', () => {
151
+ // El check `agents.native` solo se emite si hay artefactos nativos que
152
+ // verificar: un registry sin `agents/` ya no produce una fila roja sin
153
+ // remedio. Se siembra uno para que el check sea genuino en vez de
154
+ // asertar sobre una fila que hoy, con razon, no existiria.
155
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.codex', 'agents'), { recursive: true });
156
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.codex', 'agents', 'demo.toml'), 'name = "demo"\n');
151
157
  writePrefs(prefsWith(['claude-code', 'opencode', 'codex']));
152
158
  const code = (0, doctor_1.runDoctor)({ cwd: tmpWork, json: true });
153
159
  const report = JSON.parse(stdout());
@@ -34,7 +34,7 @@ describe('pin/unpin (editores de preferences)', () => {
34
34
  it('setPin escribe pins.base normalizado (acepta prefijo v)', () => {
35
35
  const { setPin } = require('../../src/commands/pin');
36
36
  setPin('base', 'v1.2.0');
37
- expect(readPrefs().pins).toEqual({ base: '1.2.0' });
37
+ expect(readPrefs().pins).toEqual({ baseline: '1.2.0' });
38
38
  });
39
39
  it('setPin acepta un registry adicional configurado', () => {
40
40
  const { writeRegistriesConfig } = require('../../src/core/registries');
@@ -66,7 +66,34 @@ describe('pin/unpin (editores de preferences)', () => {
66
66
  writeRegistriesConfig([{ name: 'equipo', remote: '/tmp/x' }]);
67
67
  setPin('equipo', '2.0.0');
68
68
  const prefs = readPrefs();
69
- expect(prefs.pins).toEqual({ base: '1.0.0', equipo: '2.0.0' });
69
+ expect(prefs.pins).toEqual({ baseline: '1.0.0', equipo: '2.0.0' });
70
70
  expect(prefs.defaultAgent).toBeDefined();
71
71
  });
72
72
  });
73
+ describe('awm pin base: el alias tiene que llegar a la clave que el resolutor lee', () => {
74
+ // Regresion: el registry base se llama `baseline` en disco, pero la ayuda
75
+ // del comando siempre documento `awm pin base <v>`. `base` se aceptaba y se
76
+ // persistia tal cual — en una clave que NADA lee. El comando reportaba
77
+ // "✓ base pinned to v1.0.0" y no cambiaba nada, incluso siendo la salida
78
+ // documentada de un bloqueo por `minCliVersion`, donde el usuario queda sin
79
+ // poder correr `awm update` ni `awm sync`.
80
+ it('setPin("base") persiste bajo "baseline", que es lo que consulta el resolutor', () => {
81
+ const { setPin } = require('../../src/commands/pin');
82
+ setPin('base', '1.5.0');
83
+ const { getPreferences } = require('../../src/utils/config');
84
+ expect(getPreferences().pins).toEqual({ baseline: '1.5.0' });
85
+ });
86
+ it('removePin("base") borra ese mismo pin (no una clave fantasma)', () => {
87
+ const { setPin, removePin } = require('../../src/commands/pin');
88
+ setPin('base', '1.5.0');
89
+ expect(removePin('base')).toBe(true);
90
+ const { getPreferences } = require('../../src/utils/config');
91
+ expect(getPreferences().pins).toEqual({});
92
+ });
93
+ it('"baseline" explicito sigue funcionando igual', () => {
94
+ const { setPin } = require('../../src/commands/pin');
95
+ setPin('baseline', '2.0.0');
96
+ const { getPreferences } = require('../../src/utils/config');
97
+ expect(getPreferences().pins).toEqual({ baseline: '2.0.0' });
98
+ });
99
+ });