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,314 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeLayout = computeLayout;
4
+ exports.syncToMiro = syncToMiro;
5
+ // Layout constants — Miro enforces min card width of 256 and auto-calculates card height (~94dp+)
6
+ const CARD_W = 260;
7
+ const CARD_H = 100; // layout spacing only — not sent to API (Miro auto-calculates)
8
+ const STORY_H = 150; // layout spacing only — generous to avoid overlap with auto-sized cards
9
+ const COL_GAP = 20;
10
+ const COL_W = CARD_W + COL_GAP; // 280
11
+ const ROW_GAP = 15;
12
+ const PADDING = 40;
13
+ const TITLE_H = 50;
14
+ const SWIMLANE_H = 40;
15
+ const SWIMLANE_GAP = 30; // extra vertical space before each swimlane separator
16
+ const ACTIVITY_GAP = 40; // horizontal gap between activity groups
17
+ function sortReleases(releases) {
18
+ return [...releases].sort((a, b) => {
19
+ if (a === 'MVP' && b !== 'MVP')
20
+ return -1;
21
+ if (b === 'MVP' && a !== 'MVP')
22
+ return 1;
23
+ if (a === 'Backlog' && b !== 'Backlog')
24
+ return 1;
25
+ if (b === 'Backlog' && a !== 'Backlog')
26
+ return -1;
27
+ return a.localeCompare(b);
28
+ });
29
+ }
30
+ function computeLayout(storyMap) {
31
+ const { activities } = storyMap;
32
+ // Guard: return empty layout when there are no activities
33
+ if (activities.length === 0) {
34
+ return { frameWidth: 0, frameHeight: 0, items: [] };
35
+ }
36
+ // USM layout: each Task gets its own column. Activities span across their Tasks.
37
+ // Stories go below their corresponding Task column.
38
+ // Activity groups are separated by ACTIVITY_GAP for visual distinction.
39
+ // Collect all unique releases (ordered)
40
+ const releaseSet = new Set();
41
+ for (const activity of activities) {
42
+ for (const task of activity.tasks) {
43
+ for (const story of task.stories) {
44
+ releaseSet.add(story.release);
45
+ }
46
+ }
47
+ }
48
+ const releases = sortReleases([...releaseSet]);
49
+ // Max stories in any single task column per release (determines row height)
50
+ const maxStoriesPerRelease = new Map();
51
+ for (const release of releases) {
52
+ let max = 0;
53
+ for (const activity of activities) {
54
+ for (const task of activity.tasks) {
55
+ const count = task.stories.filter(s => s.release === release).length;
56
+ if (count > max)
57
+ max = count;
58
+ }
59
+ }
60
+ maxStoriesPerRelease.set(release, max);
61
+ }
62
+ // Precompute activity group widths and X offsets
63
+ // Each group: numTasks * COL_W - COL_GAP, separated by ACTIVITY_GAP
64
+ const activityGroupWidths = activities.map(a => Math.max(1, a.tasks.length) * COL_W - COL_GAP);
65
+ const contentWidth = activityGroupWidths.reduce((sum, w) => sum + w, 0)
66
+ + (activities.length - 1) * ACTIVITY_GAP;
67
+ // Frame dimensions
68
+ const frameWidth = PADDING * 2 + contentWidth;
69
+ const releaseSectionH = releases.reduce((sum, r) => {
70
+ const maxStories = maxStoriesPerRelease.get(r) ?? 0;
71
+ return sum + SWIMLANE_GAP + SWIMLANE_H + maxStories * (STORY_H + ROW_GAP);
72
+ }, 0);
73
+ // Activity row + Task row + release sections
74
+ const frameHeight = PADDING + TITLE_H + CARD_H + ROW_GAP + CARD_H + ROW_GAP + releaseSectionH + PADDING;
75
+ // Frame top-left (frame centered at canvas origin)
76
+ const frameLeft = -frameWidth / 2;
77
+ const frameTop = -frameHeight / 2;
78
+ const items = [];
79
+ // Fixed Y positions
80
+ const activityY = frameTop + PADDING + TITLE_H + CARD_H / 2;
81
+ const taskY = activityY + CARD_H / 2 + ROW_GAP + CARD_H / 2;
82
+ // Build columns with ACTIVITY_GAP between groups
83
+ // Track the X offset for each task column (needed for stories later)
84
+ const taskColXPositions = []; // one per task across all activities
85
+ let groupLeftX = PADDING; // running X from frame left edge
86
+ for (let ai = 0; ai < activities.length; ai++) {
87
+ const activity = activities[ai];
88
+ const numTaskCols = Math.max(1, activity.tasks.length);
89
+ const groupWidth = activityGroupWidths[ai];
90
+ // Activity card — spans its task columns
91
+ const activityX = frameLeft + groupLeftX + groupWidth / 2;
92
+ items.push({
93
+ kind: 'activity',
94
+ title: activity.title,
95
+ x: activityX,
96
+ y: activityY,
97
+ width: groupWidth,
98
+ height: CARD_H,
99
+ color: '#ffdc4a',
100
+ });
101
+ // Task cards — one per column within this group
102
+ for (let j = 0; j < activity.tasks.length; j++) {
103
+ const colX = frameLeft + groupLeftX + j * COL_W + CARD_W / 2;
104
+ taskColXPositions.push(colX);
105
+ items.push({
106
+ kind: 'task',
107
+ title: activity.tasks[j].title,
108
+ x: colX,
109
+ y: taskY,
110
+ width: CARD_W,
111
+ height: CARD_H,
112
+ color: '#659df2',
113
+ });
114
+ }
115
+ // Activity with 0 tasks: placeholder column (no task card, but reserve X space)
116
+ if (activity.tasks.length === 0) {
117
+ taskColXPositions.push(frameLeft + groupLeftX + CARD_W / 2);
118
+ }
119
+ groupLeftX += groupWidth + ACTIVITY_GAP;
120
+ }
121
+ // Swimlanes and stories — each story goes below its Task column
122
+ let swimlaneTop = frameTop + PADDING + TITLE_H + CARD_H + ROW_GAP + CARD_H + ROW_GAP;
123
+ for (const release of releases) {
124
+ const maxStories = maxStoriesPerRelease.get(release) ?? 0;
125
+ // Extra gap before each swimlane
126
+ swimlaneTop += SWIMLANE_GAP;
127
+ const swimlaneCenterY = swimlaneTop + SWIMLANE_H / 2;
128
+ items.push({
129
+ kind: 'swimlane',
130
+ title: release,
131
+ x: 0,
132
+ y: swimlaneCenterY,
133
+ width: frameWidth,
134
+ height: SWIMLANE_H,
135
+ });
136
+ // Stories per task column (using precomputed X positions)
137
+ let colIdx = 0;
138
+ for (const activity of activities) {
139
+ if (activity.tasks.length === 0) {
140
+ colIdx++;
141
+ continue;
142
+ }
143
+ for (const task of activity.tasks) {
144
+ const colX = taskColXPositions[colIdx];
145
+ const releaseStories = task.stories.filter(s => s.release === release);
146
+ for (let k = 0; k < releaseStories.length; k++) {
147
+ const storyY = swimlaneTop + SWIMLANE_H + k * (STORY_H + ROW_GAP) + STORY_H / 2;
148
+ items.push({
149
+ kind: 'story',
150
+ title: releaseStories[k].title,
151
+ x: colX,
152
+ y: storyY,
153
+ width: CARD_W,
154
+ height: STORY_H,
155
+ color: '#ffffff',
156
+ });
157
+ }
158
+ colIdx++;
159
+ }
160
+ }
161
+ swimlaneTop += SWIMLANE_H + maxStories * (STORY_H + ROW_GAP);
162
+ }
163
+ return { frameWidth, frameHeight, items };
164
+ }
165
+ // ─── REST Client ─────────────────────────────────────────────────────────────
166
+ const MIRO_BASE = 'https://api.miro.com/v2';
167
+ async function miroRequest(config, method, path, body) {
168
+ const controller = new AbortController();
169
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
170
+ try {
171
+ const response = await fetch(`${MIRO_BASE}${path}`, {
172
+ method,
173
+ headers: {
174
+ 'Authorization': `Bearer ${config.token}`,
175
+ 'Content-Type': 'application/json',
176
+ 'Accept': 'application/json',
177
+ },
178
+ body: body ? JSON.stringify(body) : undefined,
179
+ signal: controller.signal,
180
+ });
181
+ if (!response.ok) {
182
+ const text = await response.text();
183
+ throw new Error(`Miro API ${method} ${path} → ${response.status}: ${text}`);
184
+ }
185
+ if (response.status === 204)
186
+ return null;
187
+ return response.json();
188
+ }
189
+ finally {
190
+ clearTimeout(timeoutId);
191
+ }
192
+ }
193
+ async function createFrame(config, title, width, height) {
194
+ // Position far from origin to avoid nesting inside any existing frame at (0,0)
195
+ const data = await miroRequest(config, 'POST', `/boards/${encodeURIComponent(config.boardId)}/frames`, {
196
+ data: { title, format: 'custom', type: 'freeform' },
197
+ style: { fillColor: '#f5f6f8' },
198
+ position: { x: 50000, y: 0, origin: 'center' },
199
+ geometry: { width, height },
200
+ });
201
+ if (!data?.id)
202
+ throw new Error('Miro API returned frame without id');
203
+ return data.id;
204
+ }
205
+ async function updateFrame(config, frameId, width, height) {
206
+ await miroRequest(config, 'PATCH', `/boards/${encodeURIComponent(config.boardId)}/frames/${frameId}`, {
207
+ geometry: { width, height },
208
+ });
209
+ }
210
+ async function createCard(config, frameId, item, offsetX, offsetY) {
211
+ const data = await miroRequest(config, 'POST', `/boards/${encodeURIComponent(config.boardId)}/cards`, {
212
+ data: { title: item.title },
213
+ style: { cardTheme: item.color },
214
+ position: { x: item.x + offsetX, y: item.y + offsetY, origin: 'center' },
215
+ geometry: { width: item.width }, // height is read-only, auto-calculated by Miro
216
+ parent: { id: frameId },
217
+ });
218
+ return data.id;
219
+ }
220
+ async function updateCard(config, cardId, item, offsetX, offsetY) {
221
+ await miroRequest(config, 'PATCH', `/boards/${encodeURIComponent(config.boardId)}/cards/${cardId}`, {
222
+ style: { cardTheme: item.color },
223
+ position: { x: item.x + offsetX, y: item.y + offsetY, origin: 'center' },
224
+ geometry: { width: item.width },
225
+ });
226
+ }
227
+ async function createText(config, frameId, item, offsetX, offsetY) {
228
+ const data = await miroRequest(config, 'POST', `/boards/${encodeURIComponent(config.boardId)}/texts`, {
229
+ data: { content: `<b>${item.title}</b>` },
230
+ style: { fillColor: '#e8e8e8', textAlign: 'left', fontSize: '14' },
231
+ position: { x: item.x + offsetX, y: item.y + offsetY, origin: 'center' },
232
+ geometry: { width: item.width }, // height not supported for text items
233
+ parent: { id: frameId },
234
+ });
235
+ return data.id;
236
+ }
237
+ async function deleteItem(config, itemId) {
238
+ await miroRequest(config, 'DELETE', `/boards/${encodeURIComponent(config.boardId)}/items/${itemId}`);
239
+ }
240
+ async function listFrameCards(config, frameId) {
241
+ const results = [];
242
+ let cursor;
243
+ do {
244
+ const url = `/boards/${encodeURIComponent(config.boardId)}/items?parent_item_id=${frameId}&type=card&limit=50${cursor ? `&cursor=${cursor}` : ''}`;
245
+ const data = await miroRequest(config, 'GET', url);
246
+ for (const item of data.data || []) {
247
+ results.push({ id: item.id, title: stripHtml(item.data?.title || '') });
248
+ }
249
+ cursor = data.cursor;
250
+ } while (cursor);
251
+ return results;
252
+ }
253
+ function stripHtml(html) {
254
+ return html.replace(/<[^>]+>/g, '').trim();
255
+ }
256
+ async function syncToMiro(config, storyMap, existingFrameId) {
257
+ const { frameWidth, frameHeight, items } = computeLayout(storyMap);
258
+ // Child items use coordinates relative to the frame's top-left corner.
259
+ // Layout engine computes coordinates centered at (0,0), so offset by half the frame dimensions.
260
+ const offsetX = frameWidth / 2;
261
+ const offsetY = frameHeight / 2;
262
+ let frameId = existingFrameId;
263
+ let created = 0;
264
+ let updated = 0;
265
+ let deleted = 0;
266
+ if (!frameId) {
267
+ // First sync: create frame + all items
268
+ frameId = await createFrame(config, `Story Map — ${storyMap.project}`, frameWidth, frameHeight);
269
+ for (const item of items) {
270
+ if (item.kind === 'swimlane') {
271
+ await createText(config, frameId, item, offsetX, offsetY);
272
+ }
273
+ else {
274
+ await createCard(config, frameId, item, offsetX, offsetY);
275
+ }
276
+ created++;
277
+ }
278
+ }
279
+ else {
280
+ // Subsequent sync: resize frame first so new positions don't exceed old boundaries,
281
+ // then diff and update cards.
282
+ await updateFrame(config, frameId, frameWidth, frameHeight);
283
+ const existingCards = await listFrameCards(config, frameId);
284
+ const existingByTitle = new Map(existingCards.map(c => [c.title, c.id]));
285
+ const newCardTitles = new Set(items.filter(i => i.kind !== 'swimlane').map(i => i.title));
286
+ // Delete cards no longer in the map
287
+ for (const existing of existingCards) {
288
+ if (!newCardTitles.has(existing.title)) {
289
+ await deleteItem(config, existing.id);
290
+ deleted++;
291
+ }
292
+ }
293
+ // Create or reposition cards from new layout
294
+ for (const item of items) {
295
+ // Note: swimlane text items are not diffed on re-sync.
296
+ // Renamed or removed releases require deleting and re-creating the frame manually.
297
+ // Full text-item diffing is a known limitation to address in a future iteration.
298
+ if (item.kind === 'swimlane')
299
+ continue;
300
+ const existingId = existingByTitle.get(item.title);
301
+ if (!existingId) {
302
+ await createCard(config, frameId, item, offsetX, offsetY);
303
+ created++;
304
+ }
305
+ else {
306
+ // Reposition and resize the existing card to match the current layout.
307
+ // This is necessary when the layout changes (new activities added, gaps adjusted, etc.)
308
+ await updateCard(config, existingId, item, offsetX, offsetY);
309
+ updated++;
310
+ }
311
+ }
312
+ }
313
+ return { frameId, created, updated, deleted };
314
+ }
@@ -0,0 +1,36 @@
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
+ exports.pinnedRepoDir = pinnedRepoDir;
7
+ exports.verifyProjectPins = verifyProjectPins;
8
+ // src/core/profile-pins.ts
9
+ //
10
+ // Gate de versión de `awm sync` — WS-3. Compara los pins del profile del
11
+ // proyecto (.awm/profile.json → registries) contra la versión checkouteada
12
+ // real de cada registry en la máquina.
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const registries_1 = require("./registries");
15
+ const versioning_1 = require("./versioning");
16
+ /** Dir del clone de un registry pineable: ~/.awm/registries/<name>. */
17
+ function pinnedRepoDir(name) {
18
+ return (0, registries_1.registryContentRoot)(name);
19
+ }
20
+ /** Verifica cada pin del proyecto contra la máquina. Lista vacía = todo en orden. */
21
+ async function verifyProjectPins(pins) {
22
+ const failures = [];
23
+ for (const [name, requiredRaw] of Object.entries(pins)) {
24
+ const required = (0, versioning_1.normalizePin)(requiredRaw);
25
+ const dir = pinnedRepoDir(name);
26
+ if (!fs_1.default.existsSync(dir)) {
27
+ failures.push({ name, required, actual: null, reason: 'missing-registry' });
28
+ continue;
29
+ }
30
+ const actual = await (0, versioning_1.currentVersion)(dir);
31
+ if (actual !== required) {
32
+ failures.push({ name, required, actual, reason: 'mismatch' });
33
+ }
34
+ }
35
+ return failures;
36
+ }
@@ -0,0 +1,146 @@
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
+ exports.findProjectRoot = findProjectRoot;
7
+ exports.readProfile = readProfile;
8
+ exports.writeProfile = writeProfile;
9
+ exports.ensureProfile = ensureProfile;
10
+ exports.addExtension = addExtension;
11
+ exports.ensureSkillsGitignored = ensureSkillsGitignored;
12
+ exports.shouldRecordExtension = shouldRecordExtension;
13
+ // src/core/profile.ts
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const providers_1 = require("../providers");
17
+ /**
18
+ * Walks up from `startDir` looking for a project root marker
19
+ * (`.git/`, `package.json`, or `.awm/profile.json`). Returns the
20
+ * absolute (realpath) directory, or null if none is found.
21
+ */
22
+ function findProjectRoot(startDir) {
23
+ let dir;
24
+ try {
25
+ dir = fs_1.default.realpathSync(path_1.default.resolve(startDir));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ // eslint-disable-next-line no-constant-condition
31
+ while (true) {
32
+ if (fs_1.default.existsSync(path_1.default.join(dir, '.git')) ||
33
+ fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) ||
34
+ fs_1.default.existsSync(path_1.default.join(dir, '.awm', 'profile.json'))) {
35
+ return dir;
36
+ }
37
+ const parent = path_1.default.dirname(dir);
38
+ if (parent === dir)
39
+ return null;
40
+ dir = parent;
41
+ }
42
+ }
43
+ function profilePath(root) {
44
+ return path_1.default.join(root, '.awm', 'profile.json');
45
+ }
46
+ const PIN_VERSION_RE = /^v?\d+\.\d+\.\d+$/;
47
+ function readProfile(root) {
48
+ const file = profilePath(root);
49
+ if (!fs_1.default.existsSync(file))
50
+ return { extensions: [] };
51
+ let raw;
52
+ try {
53
+ raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
54
+ }
55
+ catch {
56
+ // JSON corrupto: comportamiento lenient histórico (perfil vacío)
57
+ return { extensions: [] };
58
+ }
59
+ const exts = Array.isArray(raw.extensions)
60
+ ? raw.extensions.filter((e) => typeof e === 'string')
61
+ : [];
62
+ const profile = { extensions: exts };
63
+ // El pin de proyecto es un contrato de versionado: malformado → error explícito
64
+ // (consistente con readRegistryManifest de WS-2), nunca silenciar.
65
+ if (raw.registries !== undefined) {
66
+ if (typeof raw.registries !== 'object' || raw.registries === null || Array.isArray(raw.registries)) {
67
+ throw new Error(`Invalid profile at ${file}: "registries" must be an object of name → version`);
68
+ }
69
+ const registries = {};
70
+ for (const [name, version] of Object.entries(raw.registries)) {
71
+ if (!name || name === '.' || name.includes('..') || /[/\\]/.test(name)) {
72
+ throw new Error(`Invalid profile at ${file}: registries key "${name}" is not a valid registry name`);
73
+ }
74
+ if (typeof version !== 'string' || !PIN_VERSION_RE.test(version)) {
75
+ throw new Error(`Invalid profile at ${file}: registries["${name}"] must be "X.Y.Z", got ${JSON.stringify(version)}`);
76
+ }
77
+ registries[name] = version.replace(/^v/, '');
78
+ }
79
+ profile.registries = registries;
80
+ }
81
+ return profile;
82
+ }
83
+ function writeProfile(root, profile) {
84
+ const dir = path_1.default.join(root, '.awm');
85
+ if (!fs_1.default.existsSync(dir))
86
+ fs_1.default.mkdirSync(dir, { recursive: true });
87
+ fs_1.default.writeFileSync(profilePath(root), JSON.stringify(profile, null, 2) + '\n', 'utf-8');
88
+ }
89
+ /**
90
+ * Ensures `.awm/profile.json` exists, creating an empty `{ extensions: [] }`
91
+ * profile when absent. Idempotent: a present profile (with any extensions) is
92
+ * left untouched. Returns true only when it created the file.
93
+ *
94
+ * Without this, a project with no detectable extensions never gets a profile
95
+ * written (`addExtension` is the only other writer, and it only runs when there
96
+ * is something to add) — so `awm init` would leave the project flagged as
97
+ * uninitialized and self-referentially suggest re-running `awm init`.
98
+ */
99
+ function ensureProfile(root) {
100
+ if (fs_1.default.existsSync(profilePath(root)))
101
+ return false;
102
+ writeProfile(root, { extensions: [] });
103
+ return true;
104
+ }
105
+ /** Adds a bundle name to the profile's extensions (deduped) and persists it. */
106
+ function addExtension(root, name) {
107
+ const profile = readProfile(root);
108
+ if (!profile.extensions.includes(name))
109
+ profile.extensions.push(name);
110
+ writeProfile(root, profile);
111
+ return profile;
112
+ }
113
+ /**
114
+ * Ensures the project's .gitignore ignores the local artifact symlinks for
115
+ * all given agents (machine-specific; rebuilt by `awm sync`). Idempotent.
116
+ */
117
+ function ensureSkillsGitignored(root, agents) {
118
+ const gi = path_1.default.join(root, '.gitignore');
119
+ const existing = fs_1.default.existsSync(gi) ? fs_1.default.readFileSync(gi, 'utf-8') : '';
120
+ const existingLines = existing.split(/\r?\n/).map((l) => l.trim());
121
+ const toIgnore = [];
122
+ for (const agent of agents) {
123
+ const provider = providers_1.PROVIDERS[agent];
124
+ for (const type of ['skill', 'workflow', 'agent']) {
125
+ const config = provider[type];
126
+ if (!config)
127
+ continue;
128
+ const entry = config.local.endsWith('/') ? config.local : `${config.local}/`;
129
+ if (!toIgnore.includes(entry))
130
+ toIgnore.push(entry);
131
+ }
132
+ }
133
+ const missing = toIgnore.filter((e) => !existingLines.some((l) => l === e || l === e.replace(/\/$/, '')));
134
+ if (missing.length === 0)
135
+ return;
136
+ const needsNewline = existing.length > 0 && !existing.endsWith('\n');
137
+ fs_1.default.appendFileSync(gi, `${needsNewline ? '\n' : ''}${missing.join('\n')}\n`);
138
+ }
139
+ /**
140
+ * A bundle is recorded as a project extension only when it is a `project`-scope
141
+ * bundle being installed locally. Baseline/ambient and global installs are not
142
+ * project extensions and stay out of `.awm/profile.json`.
143
+ */
144
+ function shouldRecordExtension(bundleScope, effective) {
145
+ return bundleScope === 'project' && effective === 'local';
146
+ }