specsmd 0.1.24 → 0.1.25

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.
@@ -1,6 +1,5 @@
1
1
  const { createWatchRuntime } = require('../runtime/watch-runtime');
2
2
  const { createInitialUIState, cycleView, cycleRunFilter } = require('./store');
3
- const { formatDashboardText } = require('./renderer');
4
3
 
5
4
  function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
6
5
  if (!error) {
@@ -33,6 +32,204 @@ function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
33
32
  };
34
33
  }
35
34
 
35
+ function truncate(value, width) {
36
+ const text = String(value ?? '');
37
+ if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
38
+ return text;
39
+ }
40
+
41
+ if (width <= 3) {
42
+ return text.slice(0, width);
43
+ }
44
+
45
+ return `${text.slice(0, width - 3)}...`;
46
+ }
47
+
48
+ function fitLines(lines, maxLines, width) {
49
+ const safeLines = (Array.isArray(lines) ? lines : [])
50
+ .map((line) => truncate(line, width));
51
+
52
+ if (safeLines.length <= maxLines) {
53
+ return safeLines;
54
+ }
55
+
56
+ const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
57
+ visible.push(truncate(`... +${safeLines.length - visible.length} more`, width));
58
+ return visible;
59
+ }
60
+
61
+ function formatTime(value) {
62
+ if (!value) {
63
+ return 'n/a';
64
+ }
65
+
66
+ const date = new Date(value);
67
+ if (Number.isNaN(date.getTime())) {
68
+ return value;
69
+ }
70
+
71
+ return date.toLocaleTimeString();
72
+ }
73
+
74
+ function buildHeaderLines(snapshot, flow, workspacePath, watchEnabled, watchStatus, lastRefreshAt, view, runFilter, width) {
75
+ const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
76
+
77
+ return [
78
+ `specsmd dashboard | ${flow.toUpperCase()} | ${projectName}`,
79
+ `path: ${workspacePath}`,
80
+ `updated: ${formatTime(lastRefreshAt)} | watch: ${watchEnabled ? watchStatus : 'off'} | view: ${view} | filter: ${runFilter}`
81
+ ].map((line) => truncate(line, width));
82
+ }
83
+
84
+ function buildErrorLines(error, width) {
85
+ if (!error) {
86
+ return [];
87
+ }
88
+
89
+ const lines = [
90
+ `[${error.code || 'ERROR'}] ${error.message || 'Unknown error'}`
91
+ ];
92
+
93
+ if (error.details) {
94
+ lines.push(`details: ${error.details}`);
95
+ }
96
+ if (error.path) {
97
+ lines.push(`path: ${error.path}`);
98
+ }
99
+ if (error.hint) {
100
+ lines.push(`hint: ${error.hint}`);
101
+ }
102
+
103
+ return lines.map((line) => truncate(line, width));
104
+ }
105
+
106
+ function buildActiveRunLines(snapshot, runFilter, width) {
107
+ if (runFilter === 'completed') {
108
+ return [truncate('Hidden by active filter: completed', width)];
109
+ }
110
+
111
+ const activeRuns = snapshot?.activeRuns || [];
112
+ if (activeRuns.length === 0) {
113
+ return [truncate('No active runs', width)];
114
+ }
115
+
116
+ const lines = [];
117
+ for (const run of activeRuns) {
118
+ const currentItem = run.currentItem || 'n/a';
119
+ const workItems = Array.isArray(run.workItems) ? run.workItems : [];
120
+ const completed = workItems.filter((item) => item.status === 'completed').length;
121
+ const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
122
+ const artifacts = [
123
+ run.hasPlan ? 'plan' : null,
124
+ run.hasWalkthrough ? 'walkthrough' : null,
125
+ run.hasTestReport ? 'test-report' : null
126
+ ].filter(Boolean).join(', ') || 'none';
127
+
128
+ lines.push(`${run.id} [${run.scope}] current: ${currentItem}`);
129
+ lines.push(`progress ${completed}/${workItems.length} done, ${inProgress} active | artifacts: ${artifacts}`);
130
+ }
131
+
132
+ return lines.map((line) => truncate(line, width));
133
+ }
134
+
135
+ function buildPendingLines(snapshot, runFilter, width) {
136
+ if (runFilter === 'completed') {
137
+ return [truncate('Hidden by active filter: completed', width)];
138
+ }
139
+
140
+ const pending = snapshot?.pendingItems || [];
141
+ if (pending.length === 0) {
142
+ return [truncate('No pending work items', width)];
143
+ }
144
+
145
+ return pending.map((item) => {
146
+ const deps = item.dependencies && item.dependencies.length > 0
147
+ ? ` deps:${item.dependencies.join(',')}`
148
+ : '';
149
+ return truncate(`${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`, width);
150
+ });
151
+ }
152
+
153
+ function buildCompletedLines(snapshot, runFilter, width) {
154
+ if (runFilter === 'active') {
155
+ return [truncate('Hidden by active filter: active', width)];
156
+ }
157
+
158
+ const completedRuns = snapshot?.completedRuns || [];
159
+ if (completedRuns.length === 0) {
160
+ return [truncate('No completed runs yet', width)];
161
+ }
162
+
163
+ return completedRuns.map((run) => {
164
+ const workItems = Array.isArray(run.workItems) ? run.workItems : [];
165
+ const completed = workItems.filter((item) => item.status === 'completed').length;
166
+ return truncate(`${run.id} [${run.scope}] ${completed}/${workItems.length} done at ${run.completedAt || 'unknown'}`, width);
167
+ });
168
+ }
169
+
170
+ function buildStatsLines(snapshot, width) {
171
+ if (!snapshot?.initialized) {
172
+ return [truncate('Waiting for .specs-fire/state.yaml initialization.', width)];
173
+ }
174
+
175
+ const stats = snapshot.stats;
176
+ return [
177
+ `intents: ${stats.completedIntents}/${stats.totalIntents} done | in_progress: ${stats.inProgressIntents} | blocked: ${stats.blockedIntents}`,
178
+ `work items: ${stats.completedWorkItems}/${stats.totalWorkItems} done | in_progress: ${stats.inProgressWorkItems} | pending: ${stats.pendingWorkItems} | blocked: ${stats.blockedWorkItems}`,
179
+ `runs: ${stats.activeRunsCount} active | ${stats.completedRuns} completed | ${stats.totalRuns} total`
180
+ ].map((line) => truncate(line, width));
181
+ }
182
+
183
+ function buildOverviewProjectLines(snapshot, width) {
184
+ if (!snapshot?.initialized) {
185
+ return [
186
+ truncate('FIRE folder detected, but state.yaml is missing.', width),
187
+ truncate('Initialize project context and this view will populate.', width)
188
+ ];
189
+ }
190
+
191
+ const project = snapshot.project || {};
192
+ const workspace = snapshot.workspace || {};
193
+
194
+ return [
195
+ `project: ${project.name || 'unknown'} | fire_version: ${project.fireVersion || snapshot.version || '0.0.0'}`,
196
+ `workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'}`,
197
+ `autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`
198
+ ].map((line) => truncate(line, width));
199
+ }
200
+
201
+ function buildOverviewIntentLines(snapshot, width) {
202
+ const intents = snapshot?.intents || [];
203
+ if (intents.length === 0) {
204
+ return [truncate('No intents found', width)];
205
+ }
206
+
207
+ return intents.map((intent) => {
208
+ const workItems = Array.isArray(intent.workItems) ? intent.workItems : [];
209
+ const done = workItems.filter((item) => item.status === 'completed').length;
210
+ return truncate(`${intent.id}: ${intent.status} (${done}/${workItems.length} work items)`, width);
211
+ });
212
+ }
213
+
214
+ function buildOverviewStandardsLines(snapshot, width) {
215
+ const expected = ['constitution', 'tech-stack', 'coding-standards', 'testing-standards', 'system-architecture'];
216
+ const actual = new Set((snapshot?.standards || []).map((item) => item.type));
217
+
218
+ return expected.map((name) => {
219
+ const marker = actual.has(name) ? '[x]' : '[ ]';
220
+ return truncate(`${marker} ${name}.md`, width);
221
+ });
222
+ }
223
+
224
+ function buildWarningsLines(snapshot, width) {
225
+ const warnings = snapshot?.warnings || [];
226
+ if (warnings.length === 0) {
227
+ return [truncate('No warnings', width)];
228
+ }
229
+
230
+ return warnings.map((warning) => truncate(warning, width));
231
+ }
232
+
36
233
  function createDashboardApp(deps) {
37
234
  const {
38
235
  React,
@@ -47,17 +244,52 @@ function createDashboardApp(deps) {
47
244
  initialError
48
245
  } = deps;
49
246
 
50
- const { Box, Text, useApp, useInput } = ink;
247
+ const { Box, Text, useApp, useInput, useStdout } = ink;
51
248
  const { useState, useEffect, useCallback } = React;
52
249
 
250
+ function SectionPanel(props) {
251
+ const {
252
+ title,
253
+ lines,
254
+ width,
255
+ maxLines,
256
+ borderColor,
257
+ marginRight,
258
+ marginBottom
259
+ } = props;
260
+
261
+ const contentWidth = Math.max(18, width - 4);
262
+ const visibleLines = fitLines(lines, maxLines, contentWidth);
263
+
264
+ return React.createElement(
265
+ Box,
266
+ {
267
+ flexDirection: 'column',
268
+ borderStyle: 'round',
269
+ borderColor: borderColor || 'gray',
270
+ paddingX: 1,
271
+ width,
272
+ marginRight: marginRight || 0,
273
+ marginBottom: marginBottom || 0
274
+ },
275
+ React.createElement(Text, { bold: true, color: 'cyan' }, truncate(title, contentWidth)),
276
+ ...visibleLines.map((line, index) => React.createElement(Text, { key: `${title}-${index}` }, line))
277
+ );
278
+ }
279
+
53
280
  function DashboardApp() {
54
281
  const { exit } = useApp();
282
+ const { stdout } = useStdout();
55
283
 
56
284
  const [snapshot, setSnapshot] = useState(initialSnapshot || null);
57
285
  const [error, setError] = useState(initialError ? toDashboardError(initialError) : null);
58
286
  const [ui, setUi] = useState(createInitialUIState());
59
287
  const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
60
288
  const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
289
+ const [terminalSize, setTerminalSize] = useState(() => ({
290
+ columns: stdout?.columns || process.stdout.columns || 120,
291
+ rows: stdout?.rows || process.stdout.rows || 40
292
+ }));
61
293
 
62
294
  const refresh = useCallback(async () => {
63
295
  try {
@@ -117,6 +349,34 @@ function createDashboardApp(deps) {
117
349
  void refresh();
118
350
  }, [refresh]);
119
351
 
352
+ useEffect(() => {
353
+ if (!stdout || typeof stdout.on !== 'function') {
354
+ setTerminalSize({
355
+ columns: process.stdout.columns || 120,
356
+ rows: process.stdout.rows || 40
357
+ });
358
+ return undefined;
359
+ }
360
+
361
+ const updateSize = () => {
362
+ setTerminalSize({
363
+ columns: stdout.columns || process.stdout.columns || 120,
364
+ rows: stdout.rows || process.stdout.rows || 40
365
+ });
366
+ };
367
+
368
+ updateSize();
369
+ stdout.on('resize', updateSize);
370
+
371
+ return () => {
372
+ if (typeof stdout.off === 'function') {
373
+ stdout.off('resize', updateSize);
374
+ } else if (typeof stdout.removeListener === 'function') {
375
+ stdout.removeListener('resize', updateSize);
376
+ }
377
+ };
378
+ }, [stdout]);
379
+
120
380
  useEffect(() => {
121
381
  if (!watchEnabled) {
122
382
  return undefined;
@@ -145,24 +405,154 @@ function createDashboardApp(deps) {
145
405
  };
146
406
  }, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
147
407
 
148
- const dashboardOutput = formatDashboardText({
408
+ const cols = Number.isFinite(terminalSize.columns) ? terminalSize.columns : (process.stdout.columns || 120);
409
+ const rows = Number.isFinite(terminalSize.rows) ? terminalSize.rows : (process.stdout.rows || 40);
410
+
411
+ const compact = cols < 110;
412
+ const veryCompact = cols < 80 || rows < 22;
413
+ const contentAreaHeight = Math.max(12, rows - (ui.showHelp ? 7 : 5));
414
+ const sectionLineLimit = compact
415
+ ? Math.max(2, Math.floor(contentAreaHeight / 5))
416
+ : Math.max(3, Math.floor(contentAreaHeight / 4));
417
+
418
+ const fullWidth = Math.max(40, cols - 1);
419
+ const leftWidth = compact ? fullWidth : Math.max(28, Math.floor((fullWidth - 1) / 2));
420
+ const rightWidth = compact ? fullWidth : Math.max(28, fullWidth - leftWidth - 1);
421
+
422
+ const headerLines = buildHeaderLines(
149
423
  snapshot,
150
- error,
151
424
  flow,
152
425
  workspacePath,
153
- view: ui.view,
154
- runFilter: ui.runFilter,
155
426
  watchEnabled,
156
427
  watchStatus,
157
- showHelp: ui.showHelp,
158
428
  lastRefreshAt,
159
- width: process.stdout.columns || 120
160
- });
429
+ ui.view,
430
+ ui.runFilter,
431
+ fullWidth - 4
432
+ );
433
+
434
+ const helpLines = ui.showHelp
435
+ ? ['q quit | r refresh | h/? help | tab switch view | 1 runs | 2 overview | f run filter']
436
+ : ['press h to show shortcuts'];
437
+
438
+ const errorLines = buildErrorLines(error, fullWidth - 4);
439
+
440
+ const leftPanels = [];
441
+ const rightPanels = [];
442
+
443
+ if (ui.view === 'overview') {
444
+ leftPanels.push({
445
+ title: 'Project + Workspace',
446
+ lines: buildOverviewProjectLines(snapshot, leftWidth - 4),
447
+ borderColor: 'green'
448
+ });
449
+ leftPanels.push({
450
+ title: 'Intent Status',
451
+ lines: buildOverviewIntentLines(snapshot, leftWidth - 4),
452
+ borderColor: 'yellow'
453
+ });
454
+
455
+ rightPanels.push({
456
+ title: 'Stats',
457
+ lines: buildStatsLines(snapshot, rightWidth - 4),
458
+ borderColor: 'magenta'
459
+ });
460
+ rightPanels.push({
461
+ title: 'Standards',
462
+ lines: buildOverviewStandardsLines(snapshot, rightWidth - 4),
463
+ borderColor: 'blue'
464
+ });
465
+ rightPanels.push({
466
+ title: 'Warnings',
467
+ lines: buildWarningsLines(snapshot, rightWidth - 4),
468
+ borderColor: 'red'
469
+ });
470
+ } else {
471
+ leftPanels.push({
472
+ title: 'Active Runs',
473
+ lines: buildActiveRunLines(snapshot, ui.runFilter, leftWidth - 4),
474
+ borderColor: 'green'
475
+ });
476
+ leftPanels.push({
477
+ title: 'Pending Queue',
478
+ lines: buildPendingLines(snapshot, ui.runFilter, leftWidth - 4),
479
+ borderColor: 'yellow'
480
+ });
481
+
482
+ rightPanels.push({
483
+ title: 'Recent Completed Runs',
484
+ lines: buildCompletedLines(snapshot, ui.runFilter, rightWidth - 4),
485
+ borderColor: 'blue'
486
+ });
487
+ rightPanels.push({
488
+ title: 'Stats',
489
+ lines: buildStatsLines(snapshot, rightWidth - 4),
490
+ borderColor: 'magenta'
491
+ });
492
+ rightPanels.push({
493
+ title: 'Warnings',
494
+ lines: buildWarningsLines(snapshot, rightWidth - 4),
495
+ borderColor: 'red'
496
+ });
497
+ }
161
498
 
162
499
  return React.createElement(
163
500
  Box,
164
- { flexDirection: 'column' },
165
- React.createElement(Text, null, dashboardOutput)
501
+ { flexDirection: 'column', width: fullWidth },
502
+ React.createElement(SectionPanel, {
503
+ title: veryCompact ? 'specsmd dashboard' : 'specsmd dashboard / FIRE',
504
+ lines: headerLines,
505
+ width: fullWidth,
506
+ maxLines: veryCompact ? 2 : 3,
507
+ borderColor: 'cyan',
508
+ marginBottom: 1
509
+ }),
510
+ error ? React.createElement(SectionPanel, {
511
+ title: 'Errors',
512
+ lines: errorLines,
513
+ width: fullWidth,
514
+ maxLines: Math.max(2, sectionLineLimit),
515
+ borderColor: 'red',
516
+ marginBottom: 1
517
+ }) : null,
518
+ React.createElement(
519
+ Box,
520
+ { flexDirection: compact ? 'column' : 'row', width: fullWidth },
521
+ React.createElement(
522
+ Box,
523
+ { flexDirection: 'column', width: leftWidth, marginRight: compact ? 0 : 1 },
524
+ ...leftPanels.map((panel, index) => React.createElement(SectionPanel, {
525
+ key: `left-${panel.title}`,
526
+ title: panel.title,
527
+ lines: panel.lines,
528
+ width: leftWidth,
529
+ maxLines: sectionLineLimit,
530
+ borderColor: panel.borderColor,
531
+ marginBottom: index === leftPanels.length - 1 ? 0 : 1
532
+ }))
533
+ ),
534
+ React.createElement(
535
+ Box,
536
+ { flexDirection: 'column', width: rightWidth },
537
+ ...rightPanels.map((panel, index) => React.createElement(SectionPanel, {
538
+ key: `right-${panel.title}`,
539
+ title: panel.title,
540
+ lines: panel.lines,
541
+ width: rightWidth,
542
+ maxLines: sectionLineLimit,
543
+ borderColor: panel.borderColor,
544
+ marginBottom: index === rightPanels.length - 1 ? 0 : 1
545
+ }))
546
+ )
547
+ ),
548
+ React.createElement(SectionPanel, {
549
+ title: 'Help',
550
+ lines: helpLines,
551
+ width: fullWidth,
552
+ maxLines: 2,
553
+ borderColor: 'gray',
554
+ marginTop: 1
555
+ })
166
556
  );
167
557
  }
168
558
 
@@ -171,5 +561,7 @@ function createDashboardApp(deps) {
171
561
 
172
562
  module.exports = {
173
563
  createDashboardApp,
174
- toDashboardError
564
+ toDashboardError,
565
+ truncate,
566
+ fitLines
175
567
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
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": {