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.
- package/README.md +62 -0
- package/dist/src/commands/doctor.js +77 -0
- package/dist/src/commands/hooks/index.js +118 -0
- package/dist/src/commands/hooks/install.js +113 -0
- package/dist/src/commands/hooks/resync.js +50 -0
- package/dist/src/commands/hooks/status.js +74 -0
- package/dist/src/commands/hooks/uninstall.js +59 -0
- package/dist/src/commands/init.js +181 -0
- package/dist/src/commands/ledger/index.js +73 -0
- package/dist/src/commands/pin.js +75 -0
- package/dist/src/commands/registry/add.js +62 -0
- package/dist/src/commands/registry/index.js +166 -0
- package/dist/src/commands/registry/install-bundles.js +36 -0
- package/dist/src/commands/registry/remove.js +18 -0
- package/dist/src/commands/registry/status.js +31 -0
- package/dist/src/commands/sensors/baseline.js +93 -0
- package/dist/src/commands/sensors/formatters/eslint.js +29 -0
- package/dist/src/commands/sensors/formatters/generic.js +8 -0
- package/dist/src/commands/sensors/formatters/semgrep.js +18 -0
- package/dist/src/commands/sensors/formatters/test.js +12 -0
- package/dist/src/commands/sensors/formatters/tsc.js +23 -0
- package/dist/src/commands/sensors/index.js +94 -0
- package/dist/src/commands/sensors/init.js +112 -0
- package/dist/src/commands/sensors/install.js +78 -0
- package/dist/src/commands/sensors/run.js +217 -0
- package/dist/src/commands/sensors/status.js +85 -0
- package/dist/src/commands/sensors/types.js +2 -0
- package/dist/src/core/bundle-install.js +116 -0
- package/dist/src/core/bundles.js +122 -0
- package/dist/src/core/cli-version.js +30 -0
- package/dist/src/core/context/materializer.js +30 -0
- package/dist/src/core/context/orchestrator.js +75 -0
- package/dist/src/core/context/project-constitution-inject.js +47 -0
- package/dist/src/core/context/provider.js +29 -0
- package/dist/src/core/context/regenerate.js +61 -0
- package/dist/src/core/context/strategies/config-instructions.js +73 -0
- package/dist/src/core/context/strategies/hook-merge.js +23 -0
- package/dist/src/core/context/strategies/strategy.js +2 -0
- package/dist/src/core/context/types.js +2 -0
- package/dist/src/core/diagnostics/checks.js +141 -0
- package/dist/src/core/diagnostics/context.js +181 -0
- package/dist/src/core/diagnostics/types.js +2 -0
- package/dist/src/core/discovery.js +135 -0
- package/dist/src/core/executor.js +41 -0
- package/dist/src/core/init/detector.js +67 -0
- package/dist/src/core/init/orchestrator.js +54 -0
- package/dist/src/core/init/steps.js +279 -0
- package/dist/src/core/init/types.js +2 -0
- package/dist/src/core/ledger/store.js +73 -0
- package/dist/src/core/ledger/types.js +2 -0
- package/dist/src/core/miro.js +314 -0
- package/dist/src/core/profile-pins.js +36 -0
- package/dist/src/core/profile.js +146 -0
- package/dist/src/core/registries.js +205 -0
- package/dist/src/core/registry.js +28 -0
- package/dist/src/core/skill-integrity.js +97 -0
- package/dist/src/core/story-map-parser.js +83 -0
- package/dist/src/core/update-check-worker.js +10 -0
- package/dist/src/core/update-check.js +106 -0
- package/dist/src/core/versioning.js +112 -0
- package/dist/src/index.js +689 -0
- package/dist/src/providers/index.js +63 -0
- package/dist/src/utils/config.js +35 -0
- package/dist/src/utils/grouping.js +51 -0
- package/dist/src/utils/registry-view.js +154 -0
- package/dist/tests/commands/doctor.test.js +98 -0
- package/dist/tests/commands/hooks/install.test.js +149 -0
- package/dist/tests/commands/hooks/resync.test.js +135 -0
- package/dist/tests/commands/hooks/router.test.js +26 -0
- package/dist/tests/commands/hooks/status.test.js +88 -0
- package/dist/tests/commands/hooks/uninstall.test.js +104 -0
- package/dist/tests/commands/init.test.js +87 -0
- package/dist/tests/commands/ledger/index.test.js +64 -0
- package/dist/tests/commands/pin.test.js +72 -0
- package/dist/tests/commands/registry/add.test.js +223 -0
- package/dist/tests/commands/registry/install-bundles.test.js +87 -0
- package/dist/tests/commands/registry/remove.test.js +49 -0
- package/dist/tests/commands/registry/status.test.js +43 -0
- package/dist/tests/commands/sensors/baseline.test.js +106 -0
- package/dist/tests/commands/sensors/formatters/eslint.test.js +32 -0
- package/dist/tests/commands/sensors/formatters/semgrep.test.js +38 -0
- package/dist/tests/commands/sensors/formatters/tsc.test.js +25 -0
- package/dist/tests/commands/sensors/index.test.js +18 -0
- package/dist/tests/commands/sensors/init.test.js +137 -0
- package/dist/tests/commands/sensors/install.test.js +60 -0
- package/dist/tests/commands/sensors/router.test.js +23 -0
- package/dist/tests/commands/sensors/run.test.js +353 -0
- package/dist/tests/commands/sensors/status.test.js +98 -0
- package/dist/tests/core/base-remote.test.js +70 -0
- package/dist/tests/core/bundle-install.test.js +198 -0
- package/dist/tests/core/bundles-multiroot.test.js +68 -0
- package/dist/tests/core/bundles-overrides.test.js +49 -0
- package/dist/tests/core/bundles.test.js +96 -0
- package/dist/tests/core/cli-version.test.js +17 -0
- package/dist/tests/core/context/materializer.test.js +46 -0
- package/dist/tests/core/context/orchestrator.test.js +127 -0
- package/dist/tests/core/context/project-constitution-inject.test.js +82 -0
- package/dist/tests/core/context/provider.test.js +45 -0
- package/dist/tests/core/context/regenerate.test.js +106 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +88 -0
- package/dist/tests/core/context/strategies/hook-merge.test.js +71 -0
- package/dist/tests/core/context/types.test.js +15 -0
- package/dist/tests/core/diagnostics/checks.test.js +208 -0
- package/dist/tests/core/diagnostics/context.test.js +170 -0
- package/dist/tests/core/discovery-multiroot.test.js +63 -0
- package/dist/tests/core/discovery-overrides.test.js +105 -0
- package/dist/tests/core/discovery.test.js +113 -0
- package/dist/tests/core/executor.test.js +44 -0
- package/dist/tests/core/init/detector.test.js +56 -0
- package/dist/tests/core/init/orchestrator.test.js +122 -0
- package/dist/tests/core/init/steps.test.js +363 -0
- package/dist/tests/core/ledger/gitignore.test.js +13 -0
- package/dist/tests/core/ledger/store.test.js +122 -0
- package/dist/tests/core/miro-layout.test.js +150 -0
- package/dist/tests/core/profile-pins.test.js +131 -0
- package/dist/tests/core/profile-registries.test.js +59 -0
- package/dist/tests/core/profile.test.js +110 -0
- package/dist/tests/core/registries-capability.test.js +52 -0
- package/dist/tests/core/registries-seed.test.js +66 -0
- package/dist/tests/core/registries-sync.test.js +205 -0
- package/dist/tests/core/registries.test.js +91 -0
- package/dist/tests/core/registry-manifest.test.js +95 -0
- package/dist/tests/core/registry-versioned-sync.test.js +113 -0
- package/dist/tests/core/registry.test.js +51 -0
- package/dist/tests/core/skill-integrity.test.js +126 -0
- package/dist/tests/core/story-map-parser.test.js +136 -0
- package/dist/tests/core/sync-gates.test.js +97 -0
- package/dist/tests/core/update-check.test.js +106 -0
- package/dist/tests/core/versioning.test.js +169 -0
- package/dist/tests/integration/pack-e2e.test.js +50 -0
- package/dist/tests/providers/hooks-config.test.js +45 -0
- package/dist/tests/providers/index.test.js +58 -0
- package/dist/tests/providers/injection-config.test.js +25 -0
- package/dist/tests/registry/b3-ledger-wiring.test.js +74 -0
- package/dist/tests/registry/catalog-consistency.test.js +47 -0
- package/dist/tests/registry/design-skills.test.js +146 -0
- package/dist/tests/registry/prose-agnostic.test.js +18 -0
- package/dist/tests/registry/sensor-packs.test.js +45 -0
- package/dist/tests/registry/skill-versions.test.js +26 -0
- package/dist/tests/registry/using-awm.test.js +39 -0
- package/dist/tests/utils/config.test.js +31 -0
- package/dist/tests/utils/grouping.test.js +43 -0
- package/dist/tests/utils/registry-view-overrides.test.js +41 -0
- package/dist/tests/utils/registry-view.test.js +156 -0
- 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
|
+
}
|