specsmd 0.1.22 → 0.1.24

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 CHANGED
@@ -89,6 +89,21 @@ During installation, select your flow:
89
89
 
90
90
  The installer detects your AI coding tools and sets up agent definitions, slash commands, and project structure for your selected flow.
91
91
 
92
+ ### Live Dashboard (FIRE)
93
+
94
+ Track FIRE state continuously from terminal:
95
+
96
+ ```bash
97
+ npx specsmd@latest dashboard
98
+ ```
99
+
100
+ Useful options:
101
+
102
+ ```bash
103
+ npx specsmd@latest dashboard --flow fire --path . --refresh-ms 1000
104
+ npx specsmd@latest dashboard --no-watch
105
+ ```
106
+
92
107
  ### Install VS Code Extension (Optional)
93
108
 
94
109
  Track your progress visually with our sidebar extension:
package/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { program } = require('commander');
4
4
  const installer = require('../lib/installer');
5
+ const dashboard = require('../lib/dashboard');
5
6
  const packageJson = require('../package.json');
6
7
 
7
8
  program
@@ -18,4 +19,16 @@ program
18
19
  .description('Uninstall specsmd from the current project')
19
20
  .action(installer.uninstall);
20
21
 
21
- program.parse(process.argv);
22
+ program
23
+ .command('dashboard')
24
+ .description('Live terminal dashboard for flow state (FIRE first)')
25
+ .option('--flow <flow>', 'Flow to inspect (fire|aidlc|simple), default auto-detect')
26
+ .option('--path <dir>', 'Workspace path', process.cwd())
27
+ .option('--refresh-ms <n>', 'Fallback refresh interval in milliseconds (default: 1000)', '1000')
28
+ .option('--no-watch', 'Render once and exit')
29
+ .action((options) => dashboard.run(options));
30
+
31
+ program.parseAsync(process.argv).catch((error) => {
32
+ console.error(error.message);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,333 @@
1
+ function normalizeStatus(status) {
2
+ if (typeof status !== 'string') {
3
+ return undefined;
4
+ }
5
+
6
+ const normalized = status.toLowerCase().trim().replace(/[-\s]+/g, '_');
7
+ const map = {
8
+ pending: 'pending',
9
+ todo: 'pending',
10
+ in_progress: 'in_progress',
11
+ inprogress: 'in_progress',
12
+ active: 'in_progress',
13
+ completed: 'completed',
14
+ complete: 'completed',
15
+ done: 'completed',
16
+ blocked: 'blocked'
17
+ };
18
+
19
+ return map[normalized];
20
+ }
21
+
22
+ function normalizeMode(mode) {
23
+ if (typeof mode !== 'string') {
24
+ return undefined;
25
+ }
26
+
27
+ const normalized = mode.toLowerCase().trim();
28
+ if (normalized === 'autopilot' || normalized === 'confirm' || normalized === 'validate') {
29
+ return normalized;
30
+ }
31
+
32
+ return undefined;
33
+ }
34
+
35
+ function normalizeScope(scope) {
36
+ if (typeof scope !== 'string') {
37
+ return undefined;
38
+ }
39
+
40
+ const normalized = scope.toLowerCase().trim();
41
+ if (normalized === 'single' || normalized === 'batch' || normalized === 'wide') {
42
+ return normalized;
43
+ }
44
+
45
+ return undefined;
46
+ }
47
+
48
+ function normalizeComplexity(complexity) {
49
+ if (typeof complexity !== 'string') {
50
+ return undefined;
51
+ }
52
+
53
+ const normalized = complexity.toLowerCase().trim();
54
+ if (normalized === 'low' || normalized === 'medium' || normalized === 'high') {
55
+ return normalized;
56
+ }
57
+
58
+ return undefined;
59
+ }
60
+
61
+ function normalizeArray(value) {
62
+ return Array.isArray(value) ? value : [];
63
+ }
64
+
65
+ function normalizeTimestamp(value) {
66
+ if (value == null) {
67
+ return undefined;
68
+ }
69
+
70
+ if (value instanceof Date) {
71
+ return value.toISOString();
72
+ }
73
+
74
+ if (typeof value === 'string') {
75
+ return value.trim() === '' ? undefined : value;
76
+ }
77
+
78
+ return String(value);
79
+ }
80
+
81
+ function parseDependencies(raw) {
82
+ if (Array.isArray(raw)) {
83
+ return raw.filter((item) => typeof item === 'string' && item.trim() !== '');
84
+ }
85
+
86
+ if (typeof raw === 'string' && raw.trim() !== '') {
87
+ return [raw.trim()];
88
+ }
89
+
90
+ return [];
91
+ }
92
+
93
+ function normalizeRunWorkItem(raw, fallbackIntentId = '') {
94
+ if (typeof raw === 'string') {
95
+ return {
96
+ id: raw,
97
+ intentId: fallbackIntentId,
98
+ mode: 'confirm',
99
+ status: 'pending',
100
+ currentPhase: undefined
101
+ };
102
+ }
103
+
104
+ if (!raw || typeof raw !== 'object') {
105
+ return {
106
+ id: '',
107
+ intentId: fallbackIntentId,
108
+ mode: 'confirm',
109
+ status: 'pending',
110
+ currentPhase: undefined
111
+ };
112
+ }
113
+
114
+ const id = typeof raw.id === 'string' ? raw.id : '';
115
+ const intentId = typeof raw.intent === 'string'
116
+ ? raw.intent
117
+ : (typeof raw.intentId === 'string' ? raw.intentId : fallbackIntentId);
118
+
119
+ const status = normalizeStatus(raw.status) || 'pending';
120
+ const mode = normalizeMode(raw.mode) || 'confirm';
121
+ const currentPhase = typeof raw.current_phase === 'string'
122
+ ? raw.current_phase
123
+ : (typeof raw.currentPhase === 'string' ? raw.currentPhase : undefined);
124
+
125
+ return {
126
+ id,
127
+ intentId,
128
+ mode,
129
+ status,
130
+ currentPhase
131
+ };
132
+ }
133
+
134
+ function normalizeState(rawState) {
135
+ const raw = rawState && typeof rawState === 'object' ? rawState : {};
136
+
137
+ const project = raw.project && typeof raw.project === 'object'
138
+ ? {
139
+ name: typeof raw.project.name === 'string' ? raw.project.name : 'Unknown',
140
+ description: typeof raw.project.description === 'string' ? raw.project.description : undefined,
141
+ created: normalizeTimestamp(raw.project.created) || new Date().toISOString(),
142
+ fireVersion: typeof raw.project.fire_version === 'string'
143
+ ? raw.project.fire_version
144
+ : (typeof raw.project.fireVersion === 'string'
145
+ ? raw.project.fireVersion
146
+ : '0.0.0')
147
+ }
148
+ : null;
149
+
150
+ const workspace = raw.workspace && typeof raw.workspace === 'object'
151
+ ? {
152
+ type: typeof raw.workspace.type === 'string' ? raw.workspace.type : 'greenfield',
153
+ structure: typeof raw.workspace.structure === 'string' ? raw.workspace.structure : 'monolith',
154
+ autonomyBias: typeof raw.workspace.autonomy_bias === 'string'
155
+ ? raw.workspace.autonomy_bias
156
+ : (typeof raw.workspace.autonomyBias === 'string' ? raw.workspace.autonomyBias : 'balanced'),
157
+ runScopePreference: normalizeScope(raw.workspace.run_scope_preference)
158
+ || normalizeScope(raw.workspace.runScopePreference)
159
+ || 'single',
160
+ scannedAt: normalizeTimestamp(raw.workspace.scanned_at)
161
+ || normalizeTimestamp(raw.workspace.scannedAt),
162
+ parts: normalizeArray(raw.workspace.parts)
163
+ }
164
+ : null;
165
+
166
+ const intents = normalizeArray(raw.intents).map((intent) => {
167
+ if (!intent || typeof intent !== 'object') {
168
+ return null;
169
+ }
170
+
171
+ const workItemsRaw = normalizeArray(intent.work_items).concat(normalizeArray(intent.workItems));
172
+
173
+ return {
174
+ id: typeof intent.id === 'string' ? intent.id : '',
175
+ title: typeof intent.title === 'string' ? intent.title : '',
176
+ status: normalizeStatus(intent.status),
177
+ workItems: workItemsRaw
178
+ .map((item) => {
179
+ if (!item || typeof item !== 'object') {
180
+ return null;
181
+ }
182
+
183
+ return {
184
+ id: typeof item.id === 'string' ? item.id : '',
185
+ status: normalizeStatus(item.status) || 'pending',
186
+ mode: normalizeMode(item.mode)
187
+ };
188
+ })
189
+ .filter(Boolean)
190
+ };
191
+ }).filter(Boolean);
192
+
193
+ const rawRuns = raw.runs && typeof raw.runs === 'object' ? raw.runs : {};
194
+
195
+ const activeRuns = normalizeArray(rawRuns.active).map((run) => {
196
+ if (!run || typeof run !== 'object') {
197
+ return null;
198
+ }
199
+
200
+ const workItemsRaw = normalizeArray(run.work_items).concat(normalizeArray(run.workItems));
201
+
202
+ return {
203
+ id: typeof run.id === 'string' ? run.id : '',
204
+ scope: normalizeScope(run.scope) || 'single',
205
+ workItems: workItemsRaw.map((item) => normalizeRunWorkItem(item)).filter((item) => item.id !== ''),
206
+ currentItem: typeof run.current_item === 'string'
207
+ ? run.current_item
208
+ : (typeof run.currentItem === 'string' ? run.currentItem : ''),
209
+ started: normalizeTimestamp(run.started) || ''
210
+ };
211
+ }).filter(Boolean);
212
+
213
+ const completedRuns = normalizeArray(rawRuns.completed).map((run) => {
214
+ if (!run || typeof run !== 'object') {
215
+ return null;
216
+ }
217
+
218
+ const fallbackIntentId = typeof run.intent === 'string' ? run.intent : '';
219
+ const workItemsRaw = normalizeArray(run.work_items).concat(normalizeArray(run.workItems));
220
+
221
+ return {
222
+ id: typeof run.id === 'string' ? run.id : '',
223
+ workItems: workItemsRaw.map((item) => normalizeRunWorkItem(item, fallbackIntentId)).filter((item) => item.id !== ''),
224
+ completed: normalizeTimestamp(run.completed) || ''
225
+ };
226
+ }).filter(Boolean);
227
+
228
+ return {
229
+ project,
230
+ workspace,
231
+ intents,
232
+ runs: {
233
+ active: activeRuns,
234
+ completed: completedRuns
235
+ }
236
+ };
237
+ }
238
+
239
+ function deriveIntentStatus(stateStatus, workItems) {
240
+ const normalizedState = normalizeStatus(stateStatus);
241
+ if (normalizedState) {
242
+ return normalizedState;
243
+ }
244
+
245
+ if (!Array.isArray(workItems) || workItems.length === 0) {
246
+ return 'pending';
247
+ }
248
+
249
+ if (workItems.every((item) => item.status === 'completed')) {
250
+ return 'completed';
251
+ }
252
+
253
+ if (workItems.some((item) => item.status === 'in_progress')) {
254
+ return 'in_progress';
255
+ }
256
+
257
+ if (workItems.some((item) => item.status === 'blocked')) {
258
+ return 'blocked';
259
+ }
260
+
261
+ return 'pending';
262
+ }
263
+
264
+ function calculateStats(intents, runs, activeRuns) {
265
+ const safeIntents = Array.isArray(intents) ? intents : [];
266
+ const safeRuns = Array.isArray(runs) ? runs : [];
267
+ const safeActiveRuns = Array.isArray(activeRuns) ? activeRuns : [];
268
+ const workItems = safeIntents.flatMap((intent) => intent.workItems || []);
269
+
270
+ return {
271
+ totalIntents: safeIntents.length,
272
+ completedIntents: safeIntents.filter((intent) => intent.status === 'completed').length,
273
+ inProgressIntents: safeIntents.filter((intent) => intent.status === 'in_progress').length,
274
+ pendingIntents: safeIntents.filter((intent) => intent.status === 'pending').length,
275
+ blockedIntents: safeIntents.filter((intent) => intent.status === 'blocked').length,
276
+ totalWorkItems: workItems.length,
277
+ completedWorkItems: workItems.filter((item) => item.status === 'completed').length,
278
+ inProgressWorkItems: workItems.filter((item) => item.status === 'in_progress').length,
279
+ pendingWorkItems: workItems.filter((item) => item.status === 'pending').length,
280
+ blockedWorkItems: workItems.filter((item) => item.status === 'blocked').length,
281
+ totalRuns: safeRuns.length,
282
+ completedRuns: safeRuns.filter((run) => run.completedAt != null).length,
283
+ activeRunsCount: safeActiveRuns.length
284
+ };
285
+ }
286
+
287
+ function buildPendingItems(intents) {
288
+ const pendingItems = [];
289
+
290
+ for (const intent of intents || []) {
291
+ for (const item of intent.workItems || []) {
292
+ if (item.status !== 'pending') {
293
+ continue;
294
+ }
295
+
296
+ pendingItems.push({
297
+ id: item.id,
298
+ title: item.title,
299
+ intentId: intent.id,
300
+ intentTitle: intent.title,
301
+ mode: item.mode,
302
+ complexity: item.complexity,
303
+ dependencies: item.dependencies || [],
304
+ filePath: item.filePath
305
+ });
306
+ }
307
+ }
308
+
309
+ pendingItems.sort((a, b) => {
310
+ const depDiff = (a.dependencies?.length || 0) - (b.dependencies?.length || 0);
311
+ if (depDiff !== 0) {
312
+ return depDiff;
313
+ }
314
+ return a.id.localeCompare(b.id);
315
+ });
316
+
317
+ return pendingItems;
318
+ }
319
+
320
+ module.exports = {
321
+ normalizeStatus,
322
+ normalizeMode,
323
+ normalizeScope,
324
+ normalizeComplexity,
325
+ normalizeArray,
326
+ normalizeTimestamp,
327
+ parseDependencies,
328
+ normalizeRunWorkItem,
329
+ normalizeState,
330
+ deriveIntentStatus,
331
+ calculateStats,
332
+ buildPendingItems
333
+ };