agentic-workflow-manager 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +62 -0
  2. package/dist/src/commands/doctor.js +77 -0
  3. package/dist/src/commands/hooks/index.js +118 -0
  4. package/dist/src/commands/hooks/install.js +113 -0
  5. package/dist/src/commands/hooks/resync.js +50 -0
  6. package/dist/src/commands/hooks/status.js +74 -0
  7. package/dist/src/commands/hooks/uninstall.js +59 -0
  8. package/dist/src/commands/init.js +181 -0
  9. package/dist/src/commands/ledger/index.js +73 -0
  10. package/dist/src/commands/pin.js +75 -0
  11. package/dist/src/commands/registry/add.js +62 -0
  12. package/dist/src/commands/registry/index.js +166 -0
  13. package/dist/src/commands/registry/install-bundles.js +36 -0
  14. package/dist/src/commands/registry/remove.js +18 -0
  15. package/dist/src/commands/registry/status.js +31 -0
  16. package/dist/src/commands/sensors/baseline.js +93 -0
  17. package/dist/src/commands/sensors/formatters/eslint.js +29 -0
  18. package/dist/src/commands/sensors/formatters/generic.js +8 -0
  19. package/dist/src/commands/sensors/formatters/semgrep.js +18 -0
  20. package/dist/src/commands/sensors/formatters/test.js +12 -0
  21. package/dist/src/commands/sensors/formatters/tsc.js +23 -0
  22. package/dist/src/commands/sensors/index.js +94 -0
  23. package/dist/src/commands/sensors/init.js +112 -0
  24. package/dist/src/commands/sensors/install.js +78 -0
  25. package/dist/src/commands/sensors/run.js +217 -0
  26. package/dist/src/commands/sensors/status.js +85 -0
  27. package/dist/src/commands/sensors/types.js +2 -0
  28. package/dist/src/core/bundle-install.js +116 -0
  29. package/dist/src/core/bundles.js +122 -0
  30. package/dist/src/core/cli-version.js +30 -0
  31. package/dist/src/core/context/materializer.js +30 -0
  32. package/dist/src/core/context/orchestrator.js +75 -0
  33. package/dist/src/core/context/project-constitution-inject.js +47 -0
  34. package/dist/src/core/context/provider.js +29 -0
  35. package/dist/src/core/context/regenerate.js +61 -0
  36. package/dist/src/core/context/strategies/config-instructions.js +73 -0
  37. package/dist/src/core/context/strategies/hook-merge.js +23 -0
  38. package/dist/src/core/context/strategies/strategy.js +2 -0
  39. package/dist/src/core/context/types.js +2 -0
  40. package/dist/src/core/diagnostics/checks.js +141 -0
  41. package/dist/src/core/diagnostics/context.js +181 -0
  42. package/dist/src/core/diagnostics/types.js +2 -0
  43. package/dist/src/core/discovery.js +135 -0
  44. package/dist/src/core/executor.js +41 -0
  45. package/dist/src/core/init/detector.js +67 -0
  46. package/dist/src/core/init/orchestrator.js +54 -0
  47. package/dist/src/core/init/steps.js +279 -0
  48. package/dist/src/core/init/types.js +2 -0
  49. package/dist/src/core/ledger/store.js +73 -0
  50. package/dist/src/core/ledger/types.js +2 -0
  51. package/dist/src/core/miro.js +314 -0
  52. package/dist/src/core/profile-pins.js +36 -0
  53. package/dist/src/core/profile.js +146 -0
  54. package/dist/src/core/registries.js +205 -0
  55. package/dist/src/core/registry.js +28 -0
  56. package/dist/src/core/skill-integrity.js +97 -0
  57. package/dist/src/core/story-map-parser.js +83 -0
  58. package/dist/src/core/update-check-worker.js +10 -0
  59. package/dist/src/core/update-check.js +106 -0
  60. package/dist/src/core/versioning.js +112 -0
  61. package/dist/src/index.js +689 -0
  62. package/dist/src/providers/index.js +63 -0
  63. package/dist/src/utils/config.js +35 -0
  64. package/dist/src/utils/grouping.js +51 -0
  65. package/dist/src/utils/registry-view.js +154 -0
  66. package/dist/tests/commands/doctor.test.js +98 -0
  67. package/dist/tests/commands/hooks/install.test.js +149 -0
  68. package/dist/tests/commands/hooks/resync.test.js +135 -0
  69. package/dist/tests/commands/hooks/router.test.js +26 -0
  70. package/dist/tests/commands/hooks/status.test.js +88 -0
  71. package/dist/tests/commands/hooks/uninstall.test.js +104 -0
  72. package/dist/tests/commands/init.test.js +87 -0
  73. package/dist/tests/commands/ledger/index.test.js +64 -0
  74. package/dist/tests/commands/pin.test.js +72 -0
  75. package/dist/tests/commands/registry/add.test.js +223 -0
  76. package/dist/tests/commands/registry/install-bundles.test.js +87 -0
  77. package/dist/tests/commands/registry/remove.test.js +49 -0
  78. package/dist/tests/commands/registry/status.test.js +43 -0
  79. package/dist/tests/commands/sensors/baseline.test.js +106 -0
  80. package/dist/tests/commands/sensors/formatters/eslint.test.js +32 -0
  81. package/dist/tests/commands/sensors/formatters/semgrep.test.js +38 -0
  82. package/dist/tests/commands/sensors/formatters/tsc.test.js +25 -0
  83. package/dist/tests/commands/sensors/index.test.js +18 -0
  84. package/dist/tests/commands/sensors/init.test.js +137 -0
  85. package/dist/tests/commands/sensors/install.test.js +60 -0
  86. package/dist/tests/commands/sensors/router.test.js +23 -0
  87. package/dist/tests/commands/sensors/run.test.js +353 -0
  88. package/dist/tests/commands/sensors/status.test.js +98 -0
  89. package/dist/tests/core/base-remote.test.js +70 -0
  90. package/dist/tests/core/bundle-install.test.js +198 -0
  91. package/dist/tests/core/bundles-multiroot.test.js +68 -0
  92. package/dist/tests/core/bundles-overrides.test.js +49 -0
  93. package/dist/tests/core/bundles.test.js +96 -0
  94. package/dist/tests/core/cli-version.test.js +17 -0
  95. package/dist/tests/core/context/materializer.test.js +46 -0
  96. package/dist/tests/core/context/orchestrator.test.js +127 -0
  97. package/dist/tests/core/context/project-constitution-inject.test.js +82 -0
  98. package/dist/tests/core/context/provider.test.js +45 -0
  99. package/dist/tests/core/context/regenerate.test.js +106 -0
  100. package/dist/tests/core/context/strategies/config-instructions.test.js +88 -0
  101. package/dist/tests/core/context/strategies/hook-merge.test.js +71 -0
  102. package/dist/tests/core/context/types.test.js +15 -0
  103. package/dist/tests/core/diagnostics/checks.test.js +208 -0
  104. package/dist/tests/core/diagnostics/context.test.js +170 -0
  105. package/dist/tests/core/discovery-multiroot.test.js +63 -0
  106. package/dist/tests/core/discovery-overrides.test.js +105 -0
  107. package/dist/tests/core/discovery.test.js +113 -0
  108. package/dist/tests/core/executor.test.js +44 -0
  109. package/dist/tests/core/init/detector.test.js +56 -0
  110. package/dist/tests/core/init/orchestrator.test.js +122 -0
  111. package/dist/tests/core/init/steps.test.js +363 -0
  112. package/dist/tests/core/ledger/gitignore.test.js +13 -0
  113. package/dist/tests/core/ledger/store.test.js +122 -0
  114. package/dist/tests/core/miro-layout.test.js +150 -0
  115. package/dist/tests/core/profile-pins.test.js +131 -0
  116. package/dist/tests/core/profile-registries.test.js +59 -0
  117. package/dist/tests/core/profile.test.js +110 -0
  118. package/dist/tests/core/registries-capability.test.js +52 -0
  119. package/dist/tests/core/registries-seed.test.js +66 -0
  120. package/dist/tests/core/registries-sync.test.js +205 -0
  121. package/dist/tests/core/registries.test.js +91 -0
  122. package/dist/tests/core/registry-manifest.test.js +95 -0
  123. package/dist/tests/core/registry-versioned-sync.test.js +113 -0
  124. package/dist/tests/core/registry.test.js +51 -0
  125. package/dist/tests/core/skill-integrity.test.js +126 -0
  126. package/dist/tests/core/story-map-parser.test.js +136 -0
  127. package/dist/tests/core/sync-gates.test.js +97 -0
  128. package/dist/tests/core/update-check.test.js +106 -0
  129. package/dist/tests/core/versioning.test.js +169 -0
  130. package/dist/tests/integration/pack-e2e.test.js +50 -0
  131. package/dist/tests/providers/hooks-config.test.js +45 -0
  132. package/dist/tests/providers/index.test.js +58 -0
  133. package/dist/tests/providers/injection-config.test.js +25 -0
  134. package/dist/tests/registry/b3-ledger-wiring.test.js +74 -0
  135. package/dist/tests/registry/catalog-consistency.test.js +47 -0
  136. package/dist/tests/registry/design-skills.test.js +146 -0
  137. package/dist/tests/registry/prose-agnostic.test.js +18 -0
  138. package/dist/tests/registry/sensor-packs.test.js +45 -0
  139. package/dist/tests/registry/skill-versions.test.js +26 -0
  140. package/dist/tests/registry/using-awm.test.js +39 -0
  141. package/dist/tests/utils/config.test.js +31 -0
  142. package/dist/tests/utils/grouping.test.js +43 -0
  143. package/dist/tests/utils/registry-view-overrides.test.js +41 -0
  144. package/dist/tests/utils/registry-view.test.js +156 -0
  145. package/package.json +49 -0
@@ -0,0 +1,126 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const skill_integrity_1 = require("../../src/core/skill-integrity");
10
+ describe('skill-integrity multi-root', () => {
11
+ let tmp;
12
+ beforeEach(() => {
13
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-integrity-'));
14
+ jest.resetModules();
15
+ });
16
+ afterEach(() => {
17
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
18
+ });
19
+ it('repairGlobalSkills re-links a dangling symlink to the FIRST root that has the skill', () => {
20
+ const skillsDir = path_1.default.join(tmp, 'installed');
21
+ const rootA = path_1.default.join(tmp, 'base');
22
+ const rootB = path_1.default.join(tmp, 'personal');
23
+ fs_1.default.mkdirSync(skillsDir, { recursive: true });
24
+ fs_1.default.mkdirSync(path_1.default.join(rootB, 'skills', 'mine'), { recursive: true });
25
+ fs_1.default.writeFileSync(path_1.default.join(rootB, 'skills', 'mine', 'SKILL.md'), '---\nname: mine\n---\n');
26
+ // dangling symlink: points to a deleted target
27
+ fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'mine'), path_1.default.join(skillsDir, 'mine'), 'dir');
28
+ const { repairGlobalSkills: repair } = require('../../src/core/skill-integrity');
29
+ const r = repair(skillsDir, [rootA, rootB]);
30
+ expect(r.relinked).toEqual(['mine']);
31
+ expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'mine'))).toBe(fs_1.default.realpathSync(path_1.default.join(rootB, 'skills', 'mine')));
32
+ });
33
+ it('classifyGlobalSkills marks dead when NO root has the skill', () => {
34
+ const skillsDir = path_1.default.join(tmp, 'installed');
35
+ fs_1.default.mkdirSync(skillsDir, { recursive: true });
36
+ fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'nope'), path_1.default.join(skillsDir, 'nope'), 'dir');
37
+ const { classifyGlobalSkills: classify } = require('../../src/core/skill-integrity');
38
+ const c = classify(skillsDir, [path_1.default.join(tmp, 'base')]);
39
+ expect(c.dead).toEqual(['nope']);
40
+ });
41
+ });
42
+ function setup() {
43
+ const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-integrity-'));
44
+ const skillsDir = path_1.default.join(tmp, 'claude-skills');
45
+ const registryContentDir = path_1.default.join(tmp, 'registry');
46
+ fs_1.default.mkdirSync(skillsDir, { recursive: true });
47
+ fs_1.default.mkdirSync(path_1.default.join(registryContentDir, 'skills'), { recursive: true });
48
+ return { skillsDir, registryContentDir };
49
+ }
50
+ function makeRegistrySkill(registryContentDir, name) {
51
+ const dir = path_1.default.join(registryContentDir, 'skills', name);
52
+ fs_1.default.mkdirSync(dir, { recursive: true });
53
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `# ${name}\n`);
54
+ return dir;
55
+ }
56
+ describe('classifyGlobalSkills', () => {
57
+ it('classifies valid / repairable / dead', () => {
58
+ const { skillsDir, registryContentDir } = setup();
59
+ // valid: symlink a una skill que existe en el registry
60
+ const okTarget = makeRegistrySkill(registryContentDir, 'alpha');
61
+ fs_1.default.symlinkSync(okTarget, path_1.default.join(skillsDir, 'alpha'), 'dir');
62
+ // repairable: symlink colgante, pero la skill SÍ existe en el registry
63
+ makeRegistrySkill(registryContentDir, 'beta');
64
+ fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
65
+ // dead: symlink colgante y la skill NO existe en el registry
66
+ fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
67
+ const result = (0, skill_integrity_1.classifyGlobalSkills)(skillsDir, [registryContentDir]);
68
+ expect(result.valid).toEqual(['alpha']);
69
+ expect(result.repairable).toEqual(['beta']);
70
+ expect(result.dead).toEqual(['gamma']);
71
+ });
72
+ it('returns empty arrays when the skills dir does not exist', () => {
73
+ const result = (0, skill_integrity_1.classifyGlobalSkills)('/nonexistent/dir', ['/also/nonexistent']);
74
+ expect(result).toEqual({ valid: [], repairable: [], dead: [] });
75
+ });
76
+ });
77
+ describe('reconcileAllSkillLinks (#4 — awm update, all providers)', () => {
78
+ it('repairs every provider dir that exists and skips absent ones', () => {
79
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
80
+ const prevHome = process.env.HOME;
81
+ process.env.HOME = home;
82
+ try {
83
+ jest.resetModules();
84
+ const { reconcileAllSkillLinks } = require('../../src/core/skill-integrity');
85
+ // OpenCode dir exists with a dead orphan; Claude dir does NOT exist.
86
+ const ocSkills = path_1.default.join(home, '.agents/skills');
87
+ fs_1.default.mkdirSync(ocSkills, { recursive: true });
88
+ fs_1.default.symlinkSync(path_1.default.join(home, 'no-such-target'), path_1.default.join(ocSkills, 'ghost'), 'dir');
89
+ const res = reconcileAllSkillLinks([path_1.default.join(home, 'no-registry')]);
90
+ const oc = res.find((r) => r.agent === 'opencode');
91
+ const cc = res.find((r) => r.agent === 'claude-code');
92
+ expect(oc).toBeTruthy();
93
+ expect(oc.result.pruned).toContain('ghost');
94
+ expect(cc).toBeFalsy(); // claude dir absent → not in results
95
+ }
96
+ finally {
97
+ process.env.HOME = prevHome;
98
+ fs_1.default.rmSync(home, { recursive: true, force: true });
99
+ }
100
+ });
101
+ });
102
+ describe('repairGlobalSkills', () => {
103
+ it('re-links repairable to registry content dir and prunes dead; valid untouched; idempotent', () => {
104
+ const { skillsDir, registryContentDir } = setup();
105
+ const okTarget = makeRegistrySkill(registryContentDir, 'alpha');
106
+ fs_1.default.symlinkSync(okTarget, path_1.default.join(skillsDir, 'alpha'), 'dir');
107
+ makeRegistrySkill(registryContentDir, 'beta');
108
+ fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
109
+ fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
110
+ const r1 = (0, skill_integrity_1.repairGlobalSkills)(skillsDir, [registryContentDir]);
111
+ expect(r1.relinked).toEqual(['beta']);
112
+ expect(r1.pruned).toEqual(['gamma']);
113
+ // beta ahora apunta al registry content dir y resuelve
114
+ expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'beta'))).toBe(true);
115
+ expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'beta')))
116
+ .toBe(fs_1.default.realpathSync(path_1.default.join(registryContentDir, 'skills', 'beta')));
117
+ // gamma podado
118
+ expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'gamma'))).toBe(false);
119
+ // alpha intacto
120
+ expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'alpha'))).toBe(true);
121
+ // idempotente: segunda corrida no cambia nada
122
+ const r2 = (0, skill_integrity_1.repairGlobalSkills)(skillsDir, [registryContentDir]);
123
+ expect(r2.relinked).toEqual([]);
124
+ expect(r2.pruned).toEqual([]);
125
+ });
126
+ });
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const story_map_parser_1 = require("../../src/core/story-map-parser");
4
+ const SAMPLE_MARKDOWN = `---
5
+ project: Portal B2B
6
+ ---
7
+
8
+ # Story Map — Portal B2B
9
+
10
+ ## Goal
11
+ > Permitir a proveedores gestionar su relación con Cencosud
12
+
13
+ ## Personas
14
+
15
+ ### Proveedor
16
+ - **Rol:** Externo
17
+
18
+ ## Backbone
19
+
20
+ ### 🟡 Acceder al portal
21
+
22
+ #### 🔵 Task: Iniciar sesión
23
+ - **[MVP]** Como proveedor, quiero iniciar sesión con mi usuario y contraseña
24
+ - **[Release 2]** Como proveedor, quiero autenticarme con SSO
25
+
26
+ #### 🔵 Task: Seleccionar país y UN
27
+ - **[MVP]** Como proveedor, quiero seleccionar mi país al ingresar
28
+
29
+ ### 🟡 Consultar mis datos
30
+
31
+ #### 🔵 Task: Ver dashboard
32
+ - **[MVP]** Como proveedor, quiero ver mis indicadores principales
33
+
34
+ ## Release Summary
35
+
36
+ | Release | Stories |
37
+ |---------|---------|
38
+ | MVP | 3 |
39
+
40
+ ## Changelog
41
+
42
+ - [2026-03-31] Sesión 1
43
+ `;
44
+ const MARKDOWN_WITH_FRAME_ID = `---
45
+ project: Portal B2B
46
+ miro_frame_id: "existing-id"
47
+ ---
48
+
49
+ # Story Map — Portal B2B
50
+ `;
51
+ describe('parseStoryMap', () => {
52
+ it('extracts project name from frontmatter', () => {
53
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
54
+ expect(result.project).toBe('Portal B2B');
55
+ });
56
+ it('extracts goal text stripping the > prefix', () => {
57
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
58
+ expect(result.goal).toBe('Permitir a proveedores gestionar su relación con Cencosud');
59
+ });
60
+ it('returns undefined miro_frame_id when not in frontmatter', () => {
61
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
62
+ expect(result.miro_frame_id).toBeUndefined();
63
+ });
64
+ it('extracts miro_frame_id from frontmatter when present', () => {
65
+ const result = (0, story_map_parser_1.parseStoryMap)(MARKDOWN_WITH_FRAME_ID);
66
+ expect(result.miro_frame_id).toBe('existing-id');
67
+ });
68
+ it('extracts activities in order', () => {
69
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
70
+ expect(result.activities).toHaveLength(2);
71
+ expect(result.activities[0].title).toBe('Acceder al portal');
72
+ expect(result.activities[1].title).toBe('Consultar mis datos');
73
+ });
74
+ it('extracts tasks under each activity', () => {
75
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
76
+ expect(result.activities[0].tasks).toHaveLength(2);
77
+ expect(result.activities[0].tasks[0].title).toBe('Iniciar sesión');
78
+ expect(result.activities[0].tasks[1].title).toBe('Seleccionar país y UN');
79
+ });
80
+ it('extracts stories with their release labels', () => {
81
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
82
+ const stories = result.activities[0].tasks[0].stories;
83
+ expect(stories).toHaveLength(2);
84
+ expect(stories[0]).toEqual({ release: 'MVP', title: 'Como proveedor, quiero iniciar sesión con mi usuario y contraseña' });
85
+ expect(stories[1]).toEqual({ release: 'Release 2', title: 'Como proveedor, quiero autenticarme con SSO' });
86
+ });
87
+ it('ignores content outside Backbone section', () => {
88
+ const result = (0, story_map_parser_1.parseStoryMap)(SAMPLE_MARKDOWN);
89
+ expect(result.activities.every(a => !a.title.includes('Release') && !a.title.includes('Changelog'))).toBe(true);
90
+ });
91
+ it('falls back to title when project is absent from frontmatter', () => {
92
+ const content = `---
93
+ goal: Some goal
94
+ ---
95
+
96
+ # Story Map — Derived From Title
97
+
98
+ ## Goal
99
+ > Some goal
100
+ `;
101
+ const result = (0, story_map_parser_1.parseStoryMap)(content);
102
+ expect(result.project).toBe('Derived From Title');
103
+ });
104
+ it('falls back to Unknown when project and title are both absent', () => {
105
+ const content = `---
106
+ goal: Some goal
107
+ ---
108
+
109
+ ## Goal
110
+ > Some goal
111
+ `;
112
+ const result = (0, story_map_parser_1.parseStoryMap)(content);
113
+ expect(result.project).toBe('Unknown');
114
+ });
115
+ });
116
+ describe('updateMiroFrameId', () => {
117
+ it('adds miro_frame_id to existing frontmatter and result parses correctly', () => {
118
+ const result = (0, story_map_parser_1.updateMiroFrameId)(SAMPLE_MARKDOWN, 'new-frame-123');
119
+ expect(result).toContain('miro_frame_id: "new-frame-123"');
120
+ // Round-trip: the updated content should parse back to a valid StoryMap with the new id
121
+ const parsed = (0, story_map_parser_1.parseStoryMap)(result);
122
+ expect(parsed.miro_frame_id).toBe('new-frame-123');
123
+ expect(parsed.project).toBe('Portal B2B');
124
+ expect(parsed.activities).toHaveLength(2);
125
+ });
126
+ it('replaces existing miro_frame_id', () => {
127
+ const result = (0, story_map_parser_1.updateMiroFrameId)(MARKDOWN_WITH_FRAME_ID, 'new-frame-456');
128
+ expect(result).toContain('miro_frame_id: "new-frame-456"');
129
+ expect(result).not.toContain('existing-id');
130
+ });
131
+ it('preserves the rest of the frontmatter and body', () => {
132
+ const result = (0, story_map_parser_1.updateMiroFrameId)(SAMPLE_MARKDOWN, 'new-frame-123');
133
+ expect(result).toContain('project: Portal B2B');
134
+ expect(result).toContain('# Story Map — Portal B2B');
135
+ });
136
+ });
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // cli/tests/core/sync-gates.test.ts
3
+ //
4
+ // Gate por unidad para verifyMinCliVersions (WS-4).
5
+ // Patrón replicado de profile-pins.test.ts: tmpHome aislado, jest.resetModules(), require tardío.
6
+ // El exit-1 del handler NO se testea aquí — el gate de pins (B1) estableció el patrón
7
+ // de testear la unidad pura; los handlers se testean por integración.
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const child_process_1 = require("child_process");
16
+ const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t -c tag.gpgSign=false ${cmd}`, { cwd, stdio: 'pipe' });
17
+ function makeRegistryWithManifest(base, name, manifest) {
18
+ const dir = path_1.default.join(base, name);
19
+ fs_1.default.mkdirSync(dir, { recursive: true });
20
+ GIT(dir, 'init -q -b main');
21
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'awm-registry.json'), JSON.stringify(manifest));
22
+ GIT(dir, 'add -A');
23
+ GIT(dir, 'commit -qm init');
24
+ return dir;
25
+ }
26
+ describe('verifyMinCliVersions (gate de awm sync / awm update)', () => {
27
+ let tmpHome;
28
+ let tmpWork;
29
+ let originalHome;
30
+ let originalAwmHome;
31
+ beforeEach(() => {
32
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-minCli-home-'));
33
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-minCli-work-'));
34
+ originalHome = process.env.HOME;
35
+ originalAwmHome = process.env.AWM_HOME;
36
+ process.env.HOME = tmpHome;
37
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
38
+ jest.resetModules();
39
+ });
40
+ afterEach(() => {
41
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
42
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
43
+ if (originalHome === undefined)
44
+ delete process.env.HOME;
45
+ else
46
+ process.env.HOME = originalHome;
47
+ if (originalAwmHome === undefined)
48
+ delete process.env.AWM_HOME;
49
+ else
50
+ process.env.AWM_HOME = originalAwmHome;
51
+ });
52
+ it('CLI igual a minCliVersion → sin failures', async () => {
53
+ const source = makeRegistryWithManifest(tmpWork, 'src', { minCliVersion: '2.0.0' });
54
+ const { writeRegistriesConfig, syncRegistries, verifyMinCliVersions } = require('../../src/core/registries');
55
+ writeRegistriesConfig([{ name: 'baseline', remote: source }]);
56
+ await syncRegistries();
57
+ // Inyectamos la versión actual como igual al mínimo requerido
58
+ expect(verifyMinCliVersions('2.0.0')).toEqual([]);
59
+ });
60
+ it('CLI mayor que minCliVersion → sin failures', async () => {
61
+ const source = makeRegistryWithManifest(tmpWork, 'src', { minCliVersion: '2.0.0' });
62
+ const { writeRegistriesConfig, syncRegistries, verifyMinCliVersions } = require('../../src/core/registries');
63
+ writeRegistriesConfig([{ name: 'baseline', remote: source }]);
64
+ await syncRegistries();
65
+ expect(verifyMinCliVersions('2.1.0')).toEqual([]);
66
+ });
67
+ it('CLI menor que minCliVersion → 1 failure con name y min', async () => {
68
+ const source = makeRegistryWithManifest(tmpWork, 'src', { minCliVersion: '2.0.0' });
69
+ const { writeRegistriesConfig, syncRegistries, verifyMinCliVersions } = require('../../src/core/registries');
70
+ writeRegistriesConfig([{ name: 'baseline', remote: source }]);
71
+ await syncRegistries();
72
+ const failures = verifyMinCliVersions('1.0.0');
73
+ expect(failures).toHaveLength(1);
74
+ expect(failures[0]).toMatchObject({ name: 'baseline', min: '2.0.0' });
75
+ });
76
+ it('registry sin minCliVersion → sin failures independientemente de la versión del CLI', async () => {
77
+ const source = makeRegistryWithManifest(tmpWork, 'src', { overrides: [] });
78
+ const { writeRegistriesConfig, syncRegistries, verifyMinCliVersions } = require('../../src/core/registries');
79
+ writeRegistriesConfig([{ name: 'baseline', remote: source }]);
80
+ await syncRegistries();
81
+ expect(verifyMinCliVersions('0.0.1')).toEqual([]);
82
+ });
83
+ it('múltiples registries: solo el que falla genera un failure', async () => {
84
+ const src1 = makeRegistryWithManifest(tmpWork, 'r1', { minCliVersion: '2.0.0' });
85
+ const src2 = makeRegistryWithManifest(tmpWork, 'r2', { minCliVersion: '1.0.0' });
86
+ const { writeRegistriesConfig, syncRegistries, verifyMinCliVersions } = require('../../src/core/registries');
87
+ writeRegistriesConfig([
88
+ { name: 'alpha', remote: src1 },
89
+ { name: 'beta', remote: src2 },
90
+ ]);
91
+ await syncRegistries();
92
+ // CLI 1.5.0: cumple con beta (>=1.0.0) pero NO con alpha (>=2.0.0)
93
+ const failures = verifyMinCliVersions('1.5.0');
94
+ expect(failures).toHaveLength(1);
95
+ expect(failures[0]).toMatchObject({ name: 'alpha', min: '2.0.0' });
96
+ });
97
+ });
@@ -0,0 +1,106 @@
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
+ // cli/tests/core/update-check.test.ts (dual-tmpdir estándar)
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ // @clack/prompts ships as ESM; mock it so Jest (CommonJS mode) can load update-check
11
+ jest.mock('@clack/prompts', () => ({
12
+ confirm: jest.fn(),
13
+ isCancel: jest.fn(),
14
+ }));
15
+ describe('update-check', () => {
16
+ let tmpHome;
17
+ let tmpWork;
18
+ let originalHome;
19
+ let originalAwmHome;
20
+ let originalNoUpdate;
21
+ beforeEach(() => {
22
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-uc-home-'));
23
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-uc-work-'));
24
+ originalHome = process.env.HOME;
25
+ originalAwmHome = process.env.AWM_HOME;
26
+ originalNoUpdate = process.env.AWM_NO_UPDATE_CHECK;
27
+ process.env.HOME = tmpHome;
28
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
29
+ delete process.env.AWM_NO_UPDATE_CHECK;
30
+ jest.resetModules();
31
+ });
32
+ afterEach(() => {
33
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
34
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
35
+ if (originalHome === undefined)
36
+ delete process.env.HOME;
37
+ else
38
+ process.env.HOME = originalHome;
39
+ if (originalAwmHome === undefined)
40
+ delete process.env.AWM_HOME;
41
+ else
42
+ process.env.AWM_HOME = originalAwmHome;
43
+ if (originalNoUpdate === undefined)
44
+ delete process.env.AWM_NO_UPDATE_CHECK;
45
+ else
46
+ process.env.AWM_NO_UPDATE_CHECK = originalNoUpdate;
47
+ jest.restoreAllMocks();
48
+ });
49
+ it('fetchLatestVersion devuelve la versión del registry npm', async () => {
50
+ const { fetchLatestVersion } = require('../../src/core/update-check');
51
+ const fakeFetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ version: '2.3.0' }) });
52
+ await expect(fetchLatestVersion(fakeFetch)).resolves.toBe('2.3.0');
53
+ });
54
+ it('fetchLatestVersion sin red → null en silencio', async () => {
55
+ const { fetchLatestVersion } = require('../../src/core/update-check');
56
+ const fakeFetch = jest.fn().mockRejectedValue(new Error('offline'));
57
+ await expect(fetchLatestVersion(fakeFetch)).resolves.toBeNull();
58
+ });
59
+ it('maybeNotifyUpdate avisa si el cache trae versión más nueva y NO refresca cache fresco', () => {
60
+ const m = require('../../src/core/update-check');
61
+ m.writeUpdateCache({ lastCheck: 1_000_000, latest: '99.0.0' });
62
+ const log = jest.spyOn(console, 'log').mockImplementation(() => { });
63
+ const spawnWorker = jest.fn();
64
+ m.maybeNotifyUpdate({ now: 1_000_000 + 1000, spawnWorker });
65
+ expect(log.mock.calls.flat().join('\n')).toContain('99.0.0');
66
+ expect(spawnWorker).not.toHaveBeenCalled();
67
+ log.mockRestore();
68
+ });
69
+ it('cache viejo (>24h) dispara refresh en background', () => {
70
+ const m = require('../../src/core/update-check');
71
+ m.writeUpdateCache({ lastCheck: 0, latest: null });
72
+ const spawnWorker = jest.fn();
73
+ m.maybeNotifyUpdate({ now: 25 * 60 * 60 * 1000, spawnWorker });
74
+ expect(spawnWorker).toHaveBeenCalledTimes(1);
75
+ });
76
+ it('AWM_NO_UPDATE_CHECK desactiva todo', () => {
77
+ process.env.AWM_NO_UPDATE_CHECK = '1';
78
+ const m = require('../../src/core/update-check');
79
+ m.writeUpdateCache({ lastCheck: 0, latest: '99.0.0' });
80
+ const log = jest.spyOn(console, 'log').mockImplementation(() => { });
81
+ const spawnWorker = jest.fn();
82
+ m.maybeNotifyUpdate({ now: Date.now(), spawnWorker });
83
+ expect(log).not.toHaveBeenCalled();
84
+ expect(spawnWorker).not.toHaveBeenCalled();
85
+ log.mockRestore();
86
+ delete process.env.AWM_NO_UPDATE_CHECK;
87
+ });
88
+ it('offerSelfUpdate corre el runner al confirmar y degrada a aviso si falla', async () => {
89
+ const m = require('../../src/core/update-check');
90
+ const runner = jest.fn().mockReturnValue({ status: 1 });
91
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
92
+ await m.offerSelfUpdate({ current: '2.0.0', latest: '2.1.0', confirmImpl: async () => true, runner });
93
+ expect(runner).toHaveBeenCalled();
94
+ expect(warn.mock.calls.flat().join('\n')).toContain('npm i -g agentic-workflow-manager');
95
+ warn.mockRestore();
96
+ });
97
+ it('offerSelfUpdate escribe cache cuando ya está actualizado (TTL reset)', async () => {
98
+ const m = require('../../src/core/update-check');
99
+ const fakeFetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ version: '2.0.0' }) });
100
+ await m.offerSelfUpdate({ current: '2.0.0', fetchImpl: fakeFetch });
101
+ const cache = m.readUpdateCache();
102
+ expect(cache).not.toBeNull();
103
+ expect(cache.latest).toBe('2.0.0');
104
+ expect(cache.lastCheck).toBeGreaterThan(0);
105
+ });
106
+ });
@@ -0,0 +1,169 @@
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
+ // cli/tests/core/versioning.test.ts
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const child_process_1 = require("child_process");
11
+ const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t -c tag.gpgSign=false ${cmd}`, { cwd, stdio: 'pipe' });
12
+ /** Repo fuente con un commit inicial y un commit+tag por cada versión dada (en orden). */
13
+ function makeTaggedRepo(base, name, versions) {
14
+ const dir = path_1.default.join(base, name);
15
+ fs_1.default.mkdirSync(dir, { recursive: true });
16
+ GIT(dir, 'init -q -b main');
17
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), 'init');
18
+ GIT(dir, 'add -A');
19
+ GIT(dir, 'commit -qm init');
20
+ for (const v of versions) {
21
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), v);
22
+ GIT(dir, 'add -A');
23
+ GIT(dir, `commit -qm ${v}`);
24
+ GIT(dir, `tag v${v}`);
25
+ }
26
+ return dir;
27
+ }
28
+ function cloneOf(source, base, name) {
29
+ const dir = path_1.default.join(base, name);
30
+ GIT(base, `clone -q ${source} ${name}`);
31
+ return dir;
32
+ }
33
+ describe('versioning core', () => {
34
+ let tmpHome;
35
+ let tmpWork;
36
+ let originalHome;
37
+ let originalAwmHome;
38
+ beforeEach(() => {
39
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ver-home-'));
40
+ tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ver-work-'));
41
+ originalHome = process.env.HOME;
42
+ originalAwmHome = process.env.AWM_HOME;
43
+ process.env.HOME = tmpHome;
44
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
45
+ jest.resetModules();
46
+ });
47
+ afterEach(() => {
48
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
49
+ fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
50
+ if (originalHome === undefined)
51
+ delete process.env.HOME;
52
+ else
53
+ process.env.HOME = originalHome;
54
+ if (originalAwmHome === undefined)
55
+ delete process.env.AWM_HOME;
56
+ else
57
+ process.env.AWM_HOME = originalAwmHome;
58
+ });
59
+ describe('resolveTargetRef', () => {
60
+ it('stable sin pin → último tag con orden semver numérico (v1.10.0 > v1.9.0)', async () => {
61
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.9.0', '1.10.0']);
62
+ const clone = cloneOf(source, tmpWork, 'clone');
63
+ const { resolveTargetRef } = require('../../src/core/versioning');
64
+ const r = await resolveTargetRef(clone, { channel: 'stable' });
65
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0' });
66
+ });
67
+ it('pin exacto gana, con y sin prefijo v', async () => {
68
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
69
+ const clone = cloneOf(source, tmpWork, 'clone');
70
+ const { resolveTargetRef } = require('../../src/core/versioning');
71
+ expect(await resolveTargetRef(clone, { pin: '1.0.0', channel: 'stable' }))
72
+ .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
73
+ expect(await resolveTargetRef(clone, { pin: 'v1.0.0', channel: 'stable' }))
74
+ .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
75
+ });
76
+ it('pin inexistente → error que lista las versiones disponibles', async () => {
77
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
78
+ const clone = cloneOf(source, tmpWork, 'clone');
79
+ const { resolveTargetRef } = require('../../src/core/versioning');
80
+ await expect(resolveTargetRef(clone, { pin: '9.9.9', channel: 'stable' }))
81
+ .rejects.toThrow(/v9\.9\.9.*v1\.0\.0/s);
82
+ });
83
+ it('sin tags + stable → head-fallback al default branch', async () => {
84
+ const source = makeTaggedRepo(tmpWork, 'src', []);
85
+ const clone = cloneOf(source, tmpWork, 'clone');
86
+ const { resolveTargetRef } = require('../../src/core/versioning');
87
+ const r = await resolveTargetRef(clone, { channel: 'stable' });
88
+ expect(r).toEqual({ kind: 'head-fallback', ref: 'main' });
89
+ });
90
+ it('canal dev → head del default branch aunque haya tags', async () => {
91
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
92
+ const clone = cloneOf(source, tmpWork, 'clone');
93
+ const { resolveTargetRef } = require('../../src/core/versioning');
94
+ const r = await resolveTargetRef(clone, { channel: 'dev' });
95
+ expect(r).toEqual({ kind: 'head', ref: 'main' });
96
+ });
97
+ it('tags no semver se ignoran', async () => {
98
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
99
+ GIT(source, 'tag latest');
100
+ GIT(source, 'tag v2.0');
101
+ GIT(source, 'tag release-3.0.0');
102
+ const clone = cloneOf(source, tmpWork, 'clone');
103
+ const { resolveTargetRef } = require('../../src/core/versioning');
104
+ const r = await resolveTargetRef(clone, { channel: 'stable' });
105
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
106
+ });
107
+ it('hace fetch: ve tags creados en el remote después del clone', async () => {
108
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
109
+ const clone = cloneOf(source, tmpWork, 'clone');
110
+ // tag nuevo en el remote, post-clone
111
+ fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), '1.1.0');
112
+ GIT(source, 'add -A');
113
+ GIT(source, 'commit -qm 1.1.0');
114
+ GIT(source, 'tag v1.1.0');
115
+ const { resolveTargetRef } = require('../../src/core/versioning');
116
+ const r = await resolveTargetRef(clone, { channel: 'stable' });
117
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0' });
118
+ });
119
+ });
120
+ describe('currentVersion', () => {
121
+ it('en checkout exacto de un tag semver → versión sin prefijo v', async () => {
122
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.2.0']);
123
+ const clone = cloneOf(source, tmpWork, 'clone');
124
+ GIT(clone, 'checkout -q v1.2.0');
125
+ const { currentVersion } = require('../../src/core/versioning');
126
+ expect(await currentVersion(clone)).toBe('1.2.0');
127
+ });
128
+ it('siguiendo un branch (sin tag exacto en HEAD) → null', async () => {
129
+ const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
130
+ // commit posterior al tag para que HEAD no coincida con ningún tag
131
+ fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), 'post');
132
+ GIT(source, 'add -A');
133
+ GIT(source, 'commit -qm post');
134
+ const clone = cloneOf(source, tmpWork, 'clone');
135
+ const { currentVersion } = require('../../src/core/versioning');
136
+ expect(await currentVersion(clone)).toBeNull();
137
+ });
138
+ it('tag exacto pero no semver → null', async () => {
139
+ const source = makeTaggedRepo(tmpWork, 'src', []);
140
+ GIT(source, 'tag release-1');
141
+ const clone = cloneOf(source, tmpWork, 'clone');
142
+ GIT(clone, 'checkout -q release-1');
143
+ const { currentVersion } = require('../../src/core/versioning');
144
+ expect(await currentVersion(clone)).toBeNull();
145
+ });
146
+ });
147
+ describe('machineVersionOpts', () => {
148
+ it('sin preferences → channel stable, sin pin', () => {
149
+ const { machineVersionOpts } = require('../../src/core/versioning');
150
+ expect(machineVersionOpts('base')).toEqual({ pin: undefined, channel: 'stable' });
151
+ });
152
+ it('lee channel dev y pin por nombre desde preferences', () => {
153
+ const awmDir = path_1.default.join(tmpHome, '.awm');
154
+ fs_1.default.mkdirSync(awmDir, { recursive: true });
155
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', channel: 'dev', pins: { base: '1.2.0', equipo: '0.3.0' } }));
156
+ const { machineVersionOpts } = require('../../src/core/versioning');
157
+ expect(machineVersionOpts('base')).toEqual({ pin: '1.2.0', channel: 'dev' });
158
+ expect(machineVersionOpts('equipo')).toEqual({ pin: '0.3.0', channel: 'dev' });
159
+ expect(machineVersionOpts('otro')).toEqual({ pin: undefined, channel: 'dev' });
160
+ });
161
+ it('preferences corruptas → defaults (stable, sin pin)', () => {
162
+ const awmDir = path_1.default.join(tmpHome, '.awm');
163
+ fs_1.default.mkdirSync(awmDir, { recursive: true });
164
+ fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), '{not json');
165
+ const { machineVersionOpts } = require('../../src/core/versioning');
166
+ expect(machineVersionOpts('base')).toEqual({ pin: undefined, channel: 'stable' });
167
+ });
168
+ });
169
+ });