specsmd 0.1.57 → 0.1.59

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.
@@ -0,0 +1,45 @@
1
+ function getSectionOrderForView(view, options = {}) {
2
+ const includeWorktrees = options.includeWorktrees === true;
3
+ const includeOtherWorktrees = options.includeOtherWorktrees === true;
4
+
5
+ if (view === 'intents') {
6
+ return ['intent-status'];
7
+ }
8
+ if (view === 'completed') {
9
+ return ['completed-runs'];
10
+ }
11
+ if (view === 'health') {
12
+ return ['standards', 'stats', 'warnings', 'error-details'];
13
+ }
14
+ if (view === 'git') {
15
+ return ['git-status', 'git-changes', 'git-commits', 'git-diff'];
16
+ }
17
+ const sections = [];
18
+ if (includeWorktrees) {
19
+ sections.push('worktrees');
20
+ }
21
+ sections.push('current-run', 'run-files');
22
+ if (includeOtherWorktrees) {
23
+ sections.push('other-worktrees-active');
24
+ }
25
+ return sections;
26
+ }
27
+
28
+ function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
29
+ const order = Array.isArray(availableSections) && availableSections.length > 0
30
+ ? availableSections
31
+ : getSectionOrderForView(view);
32
+ if (order.length === 0) {
33
+ return currentSectionKey;
34
+ }
35
+
36
+ const currentIndex = order.indexOf(currentSectionKey);
37
+ const safeIndex = currentIndex >= 0 ? currentIndex : 0;
38
+ const nextIndex = (safeIndex + direction + order.length) % order.length;
39
+ return order[nextIndex];
40
+ }
41
+
42
+ module.exports = {
43
+ getSectionOrderForView,
44
+ cycleSection
45
+ };
@@ -0,0 +1,229 @@
1
+ const path = require('path');
2
+ const { clampIndex } = require('./helpers');
3
+ const { truncate } = require('./helpers');
4
+ const { getEffectiveFlow, getCurrentBolt } = require('./flow-builders');
5
+ const { collectAidlcBoltFiles } = require('./file-entries');
6
+ const { collectFireRunFiles } = require('./file-entries');
7
+
8
+ function getDashboardWorktreeMeta(snapshot) {
9
+ if (!snapshot || typeof snapshot !== 'object') {
10
+ return null;
11
+ }
12
+ const meta = snapshot.dashboardWorktrees;
13
+ if (!meta || typeof meta !== 'object') {
14
+ return null;
15
+ }
16
+ const items = Array.isArray(meta.items) ? meta.items : [];
17
+ if (items.length === 0) {
18
+ return null;
19
+ }
20
+ return {
21
+ ...meta,
22
+ items
23
+ };
24
+ }
25
+
26
+ function getWorktreeItems(snapshot) {
27
+ return getDashboardWorktreeMeta(snapshot)?.items || [];
28
+ }
29
+
30
+ function getSelectedWorktree(snapshot) {
31
+ const meta = getDashboardWorktreeMeta(snapshot);
32
+ if (!meta) {
33
+ return null;
34
+ }
35
+ return meta.items.find((item) => item.id === meta.selectedWorktreeId) || null;
36
+ }
37
+
38
+ function hasMultipleWorktrees(snapshot) {
39
+ return getWorktreeItems(snapshot).length > 1;
40
+ }
41
+
42
+ function isSelectedWorktreeMain(snapshot) {
43
+ const selected = getSelectedWorktree(snapshot);
44
+ return Boolean(selected?.isMainBranch);
45
+ }
46
+
47
+ function getWorktreeDisplayName(worktree) {
48
+ if (!worktree || typeof worktree !== 'object') {
49
+ return 'unknown';
50
+ }
51
+ if (typeof worktree.displayBranch === 'string' && worktree.displayBranch.trim() !== '') {
52
+ return worktree.displayBranch;
53
+ }
54
+ if (typeof worktree.branch === 'string' && worktree.branch.trim() !== '') {
55
+ return worktree.branch;
56
+ }
57
+ if (typeof worktree.name === 'string' && worktree.name.trim() !== '') {
58
+ return worktree.name;
59
+ }
60
+ return path.basename(worktree.path || '') || 'unknown';
61
+ }
62
+
63
+ function buildWorktreeRows(snapshot, flow) {
64
+ const meta = getDashboardWorktreeMeta(snapshot);
65
+ if (!meta) {
66
+ return [{
67
+ kind: 'info',
68
+ key: 'worktrees:none',
69
+ label: 'No git worktrees detected',
70
+ selectable: false
71
+ }];
72
+ }
73
+
74
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
75
+ const entityLabel = effectiveFlow === 'aidlc'
76
+ ? 'active bolts'
77
+ : (effectiveFlow === 'simple' ? 'active specs' : 'active runs');
78
+
79
+ const rows = [];
80
+ for (const item of meta.items) {
81
+ const currentLabel = item.isSelected ? '[CURRENT] ' : '';
82
+ const mainLabel = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
83
+ const availabilityLabel = item.flowAvailable ? '' : ' (flow unavailable)';
84
+ const statusLabel = item.status === 'loading'
85
+ ? ' loading...'
86
+ : (item.status === 'error' ? ' error' : ` ${item.activeCount || 0} ${entityLabel}`);
87
+ const scopeLabel = item.name ? ` (${item.name})` : '';
88
+
89
+ rows.push({
90
+ kind: 'info',
91
+ key: `worktree:item:${item.id}`,
92
+ label: `${currentLabel}${mainLabel}${getWorktreeDisplayName(item)}${scopeLabel}${availabilityLabel}${statusLabel}`,
93
+ color: item.isSelected ? 'green' : (item.flowAvailable ? 'gray' : 'red'),
94
+ bold: item.isSelected,
95
+ selectable: true
96
+ });
97
+ }
98
+
99
+ return rows;
100
+ }
101
+
102
+ function buildOtherWorktreeActiveGroups(snapshot, flow) {
103
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
104
+ if (effectiveFlow === 'simple') {
105
+ return [];
106
+ }
107
+
108
+ const meta = getDashboardWorktreeMeta(snapshot);
109
+ if (!meta) {
110
+ return [];
111
+ }
112
+
113
+ const selectedWorktree = getSelectedWorktree(snapshot);
114
+ if (!selectedWorktree || !selectedWorktree.isMainBranch) {
115
+ return [];
116
+ }
117
+
118
+ const groups = [];
119
+ const otherItems = meta.items.filter((item) => item.id !== meta.selectedWorktreeId);
120
+ for (const item of otherItems) {
121
+ if (!item.flowAvailable || item.status === 'unavailable' || item.status === 'error') {
122
+ continue;
123
+ }
124
+
125
+ if (effectiveFlow === 'aidlc') {
126
+ const activeBolts = Array.isArray(item.activity?.activeBolts) ? item.activity.activeBolts : [];
127
+ for (const bolt of activeBolts) {
128
+ const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
129
+ const completedStages = stages.filter((stage) => stage?.status === 'completed').length;
130
+ groups.push({
131
+ key: `other:wt:${item.id}:bolt:${bolt.id}`,
132
+ label: `[WT ${getWorktreeDisplayName(item)}] ${bolt.id} [${bolt.type || 'bolt'}] ${completedStages}/${stages.length || 0} stages`,
133
+ files: collectAidlcBoltFiles(bolt).map((file) => ({ ...file, scope: 'active' }))
134
+ });
135
+ }
136
+ continue;
137
+ }
138
+
139
+ const activeRuns = Array.isArray(item.activity?.activeRuns) ? item.activity.activeRuns : [];
140
+ for (const run of activeRuns) {
141
+ const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
142
+ const completed = workItems.filter((workItem) => workItem?.status === 'completed').length;
143
+ groups.push({
144
+ key: `other:wt:${item.id}:run:${run.id}`,
145
+ label: `[WT ${getWorktreeDisplayName(item)}] ${run.id} [${run.scope || 'single'}] ${completed}/${workItems.length} items`,
146
+ files: collectFireRunFiles(run).map((file) => ({ ...file, scope: 'active' }))
147
+ });
148
+ }
149
+ }
150
+
151
+ return groups;
152
+ }
153
+
154
+ function getOtherWorktreeEmptyMessage(snapshot, flow) {
155
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
156
+ if (!hasMultipleWorktrees(snapshot)) {
157
+ return 'No additional worktrees';
158
+ }
159
+ if (!isSelectedWorktreeMain(snapshot)) {
160
+ return 'Switch to main worktree to view active items from other worktrees';
161
+ }
162
+ if (effectiveFlow === 'aidlc') {
163
+ return 'No active bolts in other worktrees';
164
+ }
165
+ if (effectiveFlow === 'simple') {
166
+ return 'No active specs in other worktrees';
167
+ }
168
+ return 'No active runs in other worktrees';
169
+ }
170
+
171
+ function buildWorktreeOverlayLines(snapshot, selectedIndex, width) {
172
+ const meta = getDashboardWorktreeMeta(snapshot);
173
+ if (!meta) {
174
+ return [{
175
+ text: truncate('No worktrees available', width),
176
+ color: 'gray',
177
+ bold: false
178
+ }];
179
+ }
180
+
181
+ const items = Array.isArray(meta.items) ? meta.items : [];
182
+ const clampedIndex = clampIndex(selectedIndex, items.length || 1);
183
+ const lines = [{
184
+ text: truncate('Use ↑/↓ and Enter to switch. Esc closes.', width),
185
+ color: 'gray',
186
+ bold: false
187
+ }];
188
+
189
+ for (let index = 0; index < items.length; index += 1) {
190
+ const item = items[index];
191
+ const marker = index === clampedIndex ? '>' : ' ';
192
+ const current = item.isSelected ? '[CURRENT] ' : '';
193
+ const main = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
194
+ const status = item.status === 'loading'
195
+ ? 'loading'
196
+ : (item.status === 'error'
197
+ ? 'error'
198
+ : (item.flowAvailable ? `${item.activeCount || 0} active` : 'flow unavailable'));
199
+ const pathLabel = item.path ? path.basename(item.path) : 'unknown';
200
+ lines.push({
201
+ text: truncate(`${marker} ${current}${main}${getWorktreeDisplayName(item)} (${pathLabel}) | ${status}`, width),
202
+ color: index === clampedIndex ? 'green' : (item.isSelected ? 'cyan' : 'gray'),
203
+ bold: index === clampedIndex || item.isSelected
204
+ });
205
+ }
206
+
207
+ if (meta.hasPendingScans) {
208
+ lines.push({
209
+ text: truncate('Background scan in progress for additional worktrees...', width),
210
+ color: 'yellow',
211
+ bold: false
212
+ });
213
+ }
214
+
215
+ return lines;
216
+ }
217
+
218
+ module.exports = {
219
+ getDashboardWorktreeMeta,
220
+ getWorktreeItems,
221
+ getSelectedWorktree,
222
+ hasMultipleWorktrees,
223
+ isSelectedWorktreeMain,
224
+ getWorktreeDisplayName,
225
+ buildWorktreeRows,
226
+ buildOtherWorktreeActiveGroups,
227
+ getOtherWorktreeEmptyMessage,
228
+ buildWorktreeOverlayLines
229
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {