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.
- package/lib/dashboard/tui/app.js +136 -3062
- package/lib/dashboard/tui/file-entries.js +382 -0
- package/lib/dashboard/tui/flow-builders.js +991 -0
- package/lib/dashboard/tui/git-builders.js +218 -0
- package/lib/dashboard/tui/helpers.js +236 -0
- package/lib/dashboard/tui/overlays.js +242 -0
- package/lib/dashboard/tui/preview.js +145 -0
- package/lib/dashboard/tui/row-builders.js +794 -0
- package/lib/dashboard/tui/sections.js +45 -0
- package/lib/dashboard/tui/worktree-builders.js +229 -0
- package/package.json +1 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { fileExists, clampIndex } = require('./helpers');
|
|
4
|
+
const { getEffectiveFlow, getCurrentRun, getCurrentBolt, getCurrentSpec } = require('./flow-builders');
|
|
5
|
+
|
|
6
|
+
function listMarkdownFiles(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
9
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
10
|
+
.map((entry) => entry.name)
|
|
11
|
+
.sort((a, b) => a.localeCompare(b));
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pushFileEntry(entries, seenPaths, candidate) {
|
|
18
|
+
if (!candidate || typeof candidate.path !== 'string' || typeof candidate.label !== 'string') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!fileExists(candidate.path)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (seenPaths.has(candidate.path)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
seenPaths.add(candidate.path);
|
|
31
|
+
entries.push({
|
|
32
|
+
path: candidate.path,
|
|
33
|
+
label: candidate.label,
|
|
34
|
+
scope: candidate.scope || 'other'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildIntentScopedLabel(snapshot, intentId, filePath, fallbackName = 'file.md') {
|
|
39
|
+
const safeIntentId = typeof intentId === 'string' && intentId.trim() !== ''
|
|
40
|
+
? intentId
|
|
41
|
+
: '';
|
|
42
|
+
const safeFallback = typeof fallbackName === 'string' && fallbackName.trim() !== ''
|
|
43
|
+
? fallbackName
|
|
44
|
+
: 'file.md';
|
|
45
|
+
|
|
46
|
+
if (typeof filePath === 'string' && filePath.trim() !== '') {
|
|
47
|
+
if (safeIntentId && typeof snapshot?.rootPath === 'string' && snapshot.rootPath.trim() !== '') {
|
|
48
|
+
const intentPath = path.join(snapshot.rootPath, 'intents', safeIntentId);
|
|
49
|
+
const relativePath = path.relative(intentPath, filePath);
|
|
50
|
+
if (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
51
|
+
return `${safeIntentId}/${relativePath.split(path.sep).join('/')}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const basename = path.basename(filePath);
|
|
56
|
+
return safeIntentId ? `${safeIntentId}/${basename}` : basename;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return safeIntentId ? `${safeIntentId}/${safeFallback}` : safeFallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findIntentIdForWorkItem(snapshot, workItemId) {
|
|
63
|
+
if (typeof workItemId !== 'string' || workItemId.trim() === '') {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
68
|
+
for (const intent of intents) {
|
|
69
|
+
const items = Array.isArray(intent?.workItems) ? intent.workItems : [];
|
|
70
|
+
if (items.some((item) => item?.id === workItemId)) {
|
|
71
|
+
return intent?.id || '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveFireWorkItemPath(snapshot, intentId, workItemId, explicitPath) {
|
|
79
|
+
if (typeof explicitPath === 'string' && explicitPath.trim() !== '') {
|
|
80
|
+
return explicitPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof snapshot?.rootPath !== 'string' || snapshot.rootPath.trim() === '') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof workItemId !== 'string' || workItemId.trim() === '') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const safeIntentId = typeof intentId === 'string' && intentId.trim() !== ''
|
|
92
|
+
? intentId
|
|
93
|
+
: findIntentIdForWorkItem(snapshot, workItemId);
|
|
94
|
+
|
|
95
|
+
if (!safeIntentId) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return path.join(snapshot.rootPath, 'intents', safeIntentId, 'work-items', `${workItemId}.md`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectFireRunFiles(run) {
|
|
103
|
+
if (!run || typeof run.folderPath !== 'string') {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const names = ['run.md'];
|
|
108
|
+
if (run.hasPlan) names.push('plan.md');
|
|
109
|
+
if (run.hasTestReport) names.push('test-report.md');
|
|
110
|
+
if (run.hasWalkthrough) names.push('walkthrough.md');
|
|
111
|
+
|
|
112
|
+
return names.map((fileName) => ({
|
|
113
|
+
label: `${run.id}/${fileName}`,
|
|
114
|
+
path: path.join(run.folderPath, fileName)
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectAidlcBoltFiles(bolt) {
|
|
119
|
+
if (!bolt || typeof bolt.path !== 'string') {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fileNames = Array.isArray(bolt.files) && bolt.files.length > 0
|
|
124
|
+
? bolt.files
|
|
125
|
+
: listMarkdownFiles(bolt.path);
|
|
126
|
+
|
|
127
|
+
return fileNames.map((fileName) => ({
|
|
128
|
+
label: `${bolt.id}/${fileName}`,
|
|
129
|
+
path: path.join(bolt.path, fileName)
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectSimpleSpecFiles(spec) {
|
|
134
|
+
if (!spec || typeof spec.path !== 'string') {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const names = [];
|
|
139
|
+
if (spec.hasRequirements) names.push('requirements.md');
|
|
140
|
+
if (spec.hasDesign) names.push('design.md');
|
|
141
|
+
if (spec.hasTasks) names.push('tasks.md');
|
|
142
|
+
|
|
143
|
+
return names.map((fileName) => ({
|
|
144
|
+
label: `${spec.name}/${fileName}`,
|
|
145
|
+
path: path.join(spec.path, fileName)
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectAidlcIntentContextFiles(snapshot, intentId) {
|
|
150
|
+
if (!snapshot || typeof intentId !== 'string' || intentId.trim() === '') {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const intentPath = path.join(snapshot.rootPath || '', 'intents', intentId);
|
|
155
|
+
return [
|
|
156
|
+
{
|
|
157
|
+
label: `${intentId}/requirements.md`,
|
|
158
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
159
|
+
scope: 'intent'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: `${intentId}/system-context.md`,
|
|
163
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
164
|
+
scope: 'intent'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: `${intentId}/units.md`,
|
|
168
|
+
path: path.join(intentPath, 'units.md'),
|
|
169
|
+
scope: 'intent'
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function filterExistingFiles(files) {
|
|
175
|
+
return (Array.isArray(files) ? files : []).filter((file) => {
|
|
176
|
+
if (!file || typeof file.path !== 'string' || typeof file.label !== 'string') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (file.allowMissing === true) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return fileExists(file.path);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getRunFileEntries(snapshot, flow, options = {}) {
|
|
187
|
+
const includeBacklog = options.includeBacklog !== false;
|
|
188
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
189
|
+
const entries = [];
|
|
190
|
+
const seenPaths = new Set();
|
|
191
|
+
|
|
192
|
+
if (effectiveFlow === 'aidlc') {
|
|
193
|
+
const bolt = getCurrentBolt(snapshot);
|
|
194
|
+
for (const file of collectAidlcBoltFiles(bolt)) {
|
|
195
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!includeBacklog) {
|
|
199
|
+
return entries;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
203
|
+
for (const pendingBolt of pendingBolts) {
|
|
204
|
+
for (const file of collectAidlcBoltFiles(pendingBolt)) {
|
|
205
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
210
|
+
for (const completedBolt of completedBolts) {
|
|
211
|
+
for (const file of collectAidlcBoltFiles(completedBolt)) {
|
|
212
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const intentIds = new Set([
|
|
217
|
+
...pendingBolts.map((item) => item?.intent).filter(Boolean),
|
|
218
|
+
...completedBolts.map((item) => item?.intent).filter(Boolean)
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
for (const intentId of intentIds) {
|
|
222
|
+
const intentPath = path.join(snapshot?.rootPath || '', 'intents', intentId);
|
|
223
|
+
pushFileEntry(entries, seenPaths, {
|
|
224
|
+
label: `${intentId}/requirements.md`,
|
|
225
|
+
path: path.join(intentPath, 'requirements.md'),
|
|
226
|
+
scope: 'intent'
|
|
227
|
+
});
|
|
228
|
+
pushFileEntry(entries, seenPaths, {
|
|
229
|
+
label: `${intentId}/system-context.md`,
|
|
230
|
+
path: path.join(intentPath, 'system-context.md'),
|
|
231
|
+
scope: 'intent'
|
|
232
|
+
});
|
|
233
|
+
pushFileEntry(entries, seenPaths, {
|
|
234
|
+
label: `${intentId}/units.md`,
|
|
235
|
+
path: path.join(intentPath, 'units.md'),
|
|
236
|
+
scope: 'intent'
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (effectiveFlow === 'simple') {
|
|
243
|
+
const spec = getCurrentSpec(snapshot);
|
|
244
|
+
for (const file of collectSimpleSpecFiles(spec)) {
|
|
245
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!includeBacklog) {
|
|
249
|
+
return entries;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
253
|
+
for (const pendingSpec of pendingSpecs) {
|
|
254
|
+
for (const file of collectSimpleSpecFiles(pendingSpec)) {
|
|
255
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
260
|
+
for (const completedSpec of completedSpecs) {
|
|
261
|
+
for (const file of collectSimpleSpecFiles(completedSpec)) {
|
|
262
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return entries;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const run = getCurrentRun(snapshot);
|
|
270
|
+
for (const file of collectFireRunFiles(run)) {
|
|
271
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!includeBacklog) {
|
|
275
|
+
return entries;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
279
|
+
for (const pendingItem of pendingItems) {
|
|
280
|
+
pushFileEntry(entries, seenPaths, {
|
|
281
|
+
label: buildIntentScopedLabel(
|
|
282
|
+
snapshot,
|
|
283
|
+
pendingItem?.intentId,
|
|
284
|
+
pendingItem?.filePath,
|
|
285
|
+
`${pendingItem?.id || 'work-item'}.md`
|
|
286
|
+
),
|
|
287
|
+
path: pendingItem?.filePath,
|
|
288
|
+
scope: 'upcoming'
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (pendingItem?.intentId) {
|
|
292
|
+
pushFileEntry(entries, seenPaths, {
|
|
293
|
+
label: buildIntentScopedLabel(
|
|
294
|
+
snapshot,
|
|
295
|
+
pendingItem.intentId,
|
|
296
|
+
path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
297
|
+
'brief.md'
|
|
298
|
+
),
|
|
299
|
+
path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
|
|
300
|
+
scope: 'intent'
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
306
|
+
for (const completedRun of completedRuns) {
|
|
307
|
+
for (const file of collectFireRunFiles(completedRun)) {
|
|
308
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
313
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
314
|
+
: [];
|
|
315
|
+
for (const intent of completedIntents) {
|
|
316
|
+
pushFileEntry(entries, seenPaths, {
|
|
317
|
+
label: buildIntentScopedLabel(
|
|
318
|
+
snapshot,
|
|
319
|
+
intent?.id,
|
|
320
|
+
path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
321
|
+
'brief.md'
|
|
322
|
+
),
|
|
323
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
|
|
324
|
+
scope: 'intent'
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return entries;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getNoFileMessage(flow) {
|
|
332
|
+
return `No selectable files for ${String(flow || 'flow').toUpperCase()}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatScope(scope) {
|
|
336
|
+
if (scope === 'active') return 'ACTIVE';
|
|
337
|
+
if (scope === 'upcoming') return 'UPNEXT';
|
|
338
|
+
if (scope === 'completed') return 'DONE';
|
|
339
|
+
if (scope === 'intent') return 'INTENT';
|
|
340
|
+
if (scope === 'staged') return 'STAGED';
|
|
341
|
+
if (scope === 'unstaged') return 'UNSTAGED';
|
|
342
|
+
if (scope === 'untracked') return 'UNTRACKED';
|
|
343
|
+
if (scope === 'conflicted') return 'CONFLICT';
|
|
344
|
+
return 'FILE';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getNoPendingMessage(flow) {
|
|
348
|
+
if (flow === 'aidlc') return 'No queued bolts';
|
|
349
|
+
if (flow === 'simple') return 'No pending specs';
|
|
350
|
+
return 'No pending work items';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getNoCompletedMessage(flow) {
|
|
354
|
+
if (flow === 'aidlc') return 'No completed bolts yet';
|
|
355
|
+
if (flow === 'simple') return 'No completed specs yet';
|
|
356
|
+
return 'No completed runs yet';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function getNoCurrentMessage(flow) {
|
|
360
|
+
if (flow === 'aidlc') return 'No active bolt';
|
|
361
|
+
if (flow === 'simple') return 'No active spec';
|
|
362
|
+
return 'No active run';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
listMarkdownFiles,
|
|
367
|
+
pushFileEntry,
|
|
368
|
+
buildIntentScopedLabel,
|
|
369
|
+
findIntentIdForWorkItem,
|
|
370
|
+
resolveFireWorkItemPath,
|
|
371
|
+
collectFireRunFiles,
|
|
372
|
+
collectAidlcBoltFiles,
|
|
373
|
+
collectSimpleSpecFiles,
|
|
374
|
+
collectAidlcIntentContextFiles,
|
|
375
|
+
filterExistingFiles,
|
|
376
|
+
getRunFileEntries,
|
|
377
|
+
getNoFileMessage,
|
|
378
|
+
formatScope,
|
|
379
|
+
getNoPendingMessage,
|
|
380
|
+
getNoCompletedMessage,
|
|
381
|
+
getNoCurrentMessage
|
|
382
|
+
};
|