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 +15 -0
- package/bin/cli.js +14 -1
- package/lib/dashboard/fire/model.js +333 -0
- package/lib/dashboard/fire/parser.js +387 -0
- package/lib/dashboard/flow-detect.js +86 -0
- package/lib/dashboard/index.js +134 -0
- package/lib/dashboard/runtime/watch-runtime.js +113 -0
- package/lib/dashboard/tui/app.js +175 -0
- package/lib/dashboard/tui/components/error-banner.js +35 -0
- package/lib/dashboard/tui/components/header.js +62 -0
- package/lib/dashboard/tui/components/help-footer.js +15 -0
- package/lib/dashboard/tui/components/stats-strip.js +35 -0
- package/lib/dashboard/tui/renderer.js +78 -0
- package/lib/dashboard/tui/store.js +30 -0
- package/lib/dashboard/tui/views/overview-view.js +61 -0
- package/lib/dashboard/tui/views/runs-view.js +98 -0
- package/lib/installers/CodexInstaller.js +72 -1
- package/package.json +6 -3
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const { createWatchRuntime } = require('../runtime/watch-runtime');
|
|
2
|
+
const { createInitialUIState, cycleView, cycleRunFilter } = require('./store');
|
|
3
|
+
const { formatDashboardText } = require('./renderer');
|
|
4
|
+
|
|
5
|
+
function toDashboardError(error, defaultCode = 'DASHBOARD_ERROR') {
|
|
6
|
+
if (!error) {
|
|
7
|
+
return {
|
|
8
|
+
code: defaultCode,
|
|
9
|
+
message: 'Unknown dashboard error.'
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof error === 'string') {
|
|
14
|
+
return {
|
|
15
|
+
code: defaultCode,
|
|
16
|
+
message: error
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof error === 'object') {
|
|
21
|
+
return {
|
|
22
|
+
code: error.code || defaultCode,
|
|
23
|
+
message: error.message || 'Unknown dashboard error.',
|
|
24
|
+
details: error.details,
|
|
25
|
+
path: error.path,
|
|
26
|
+
hint: error.hint
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
code: defaultCode,
|
|
32
|
+
message: String(error)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createDashboardApp(deps) {
|
|
37
|
+
const {
|
|
38
|
+
React,
|
|
39
|
+
ink,
|
|
40
|
+
parseSnapshot,
|
|
41
|
+
workspacePath,
|
|
42
|
+
rootPath,
|
|
43
|
+
flow,
|
|
44
|
+
refreshMs,
|
|
45
|
+
watchEnabled,
|
|
46
|
+
initialSnapshot,
|
|
47
|
+
initialError
|
|
48
|
+
} = deps;
|
|
49
|
+
|
|
50
|
+
const { Box, Text, useApp, useInput } = ink;
|
|
51
|
+
const { useState, useEffect, useCallback } = React;
|
|
52
|
+
|
|
53
|
+
function DashboardApp() {
|
|
54
|
+
const { exit } = useApp();
|
|
55
|
+
|
|
56
|
+
const [snapshot, setSnapshot] = useState(initialSnapshot || null);
|
|
57
|
+
const [error, setError] = useState(initialError ? toDashboardError(initialError) : null);
|
|
58
|
+
const [ui, setUi] = useState(createInitialUIState());
|
|
59
|
+
const [lastRefreshAt, setLastRefreshAt] = useState(new Date().toISOString());
|
|
60
|
+
const [watchStatus, setWatchStatus] = useState(watchEnabled ? 'watching' : 'off');
|
|
61
|
+
|
|
62
|
+
const refresh = useCallback(async () => {
|
|
63
|
+
try {
|
|
64
|
+
const result = await parseSnapshot();
|
|
65
|
+
|
|
66
|
+
if (result?.ok) {
|
|
67
|
+
setSnapshot(result.snapshot || null);
|
|
68
|
+
setError(null);
|
|
69
|
+
setWatchStatus(watchEnabled ? 'watching' : 'off');
|
|
70
|
+
} else {
|
|
71
|
+
setError(toDashboardError(result?.error, 'PARSE_ERROR'));
|
|
72
|
+
}
|
|
73
|
+
} catch (refreshError) {
|
|
74
|
+
setError(toDashboardError(refreshError, 'REFRESH_FAILED'));
|
|
75
|
+
} finally {
|
|
76
|
+
setLastRefreshAt(new Date().toISOString());
|
|
77
|
+
}
|
|
78
|
+
}, [parseSnapshot, watchEnabled]);
|
|
79
|
+
|
|
80
|
+
useInput((input, key) => {
|
|
81
|
+
if ((key.ctrl && input === 'c') || input === 'q') {
|
|
82
|
+
exit();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input === 'r') {
|
|
87
|
+
void refresh();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (input === 'h' || input === '?') {
|
|
92
|
+
setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (input === '1') {
|
|
97
|
+
setUi((previous) => ({ ...previous, view: 'runs' }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (input === '2') {
|
|
102
|
+
setUi((previous) => ({ ...previous, view: 'overview' }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (key.tab) {
|
|
107
|
+
setUi((previous) => ({ ...previous, view: cycleView(previous.view) }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (input === 'f') {
|
|
112
|
+
setUi((previous) => ({ ...previous, runFilter: cycleRunFilter(previous.runFilter) }));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
void refresh();
|
|
118
|
+
}, [refresh]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!watchEnabled) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const runtime = createWatchRuntime({
|
|
126
|
+
rootPath: rootPath || `${workspacePath}/.specs-fire`,
|
|
127
|
+
debounceMs: 250,
|
|
128
|
+
onRefresh: () => {
|
|
129
|
+
void refresh();
|
|
130
|
+
},
|
|
131
|
+
onError: (watchError) => {
|
|
132
|
+
setWatchStatus('reconnecting');
|
|
133
|
+
setError(toDashboardError(watchError, 'WATCH_ERROR'));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
runtime.start();
|
|
138
|
+
const interval = setInterval(() => {
|
|
139
|
+
void refresh();
|
|
140
|
+
}, refreshMs);
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
clearInterval(interval);
|
|
144
|
+
void runtime.close();
|
|
145
|
+
};
|
|
146
|
+
}, [watchEnabled, refreshMs, refresh, rootPath, workspacePath]);
|
|
147
|
+
|
|
148
|
+
const dashboardOutput = formatDashboardText({
|
|
149
|
+
snapshot,
|
|
150
|
+
error,
|
|
151
|
+
flow,
|
|
152
|
+
workspacePath,
|
|
153
|
+
view: ui.view,
|
|
154
|
+
runFilter: ui.runFilter,
|
|
155
|
+
watchEnabled,
|
|
156
|
+
watchStatus,
|
|
157
|
+
showHelp: ui.showHelp,
|
|
158
|
+
lastRefreshAt,
|
|
159
|
+
width: process.stdout.columns || 120
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return React.createElement(
|
|
163
|
+
Box,
|
|
164
|
+
{ flexDirection: 'column' },
|
|
165
|
+
React.createElement(Text, null, dashboardOutput)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return DashboardApp;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
createDashboardApp,
|
|
174
|
+
toDashboardError
|
|
175
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { truncate } = require('./header');
|
|
2
|
+
|
|
3
|
+
function renderErrorLines(error, width, watchEnabled = true) {
|
|
4
|
+
if (!error) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const lines = [
|
|
9
|
+
`[error:${error.code || 'UNKNOWN'}] ${error.message || 'Unknown error'}`
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
if (error.details) {
|
|
13
|
+
lines.push(`details: ${error.details}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (error.path) {
|
|
17
|
+
lines.push(`path: ${error.path}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (error.hint) {
|
|
21
|
+
lines.push(`hint: ${error.hint}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (watchEnabled) {
|
|
25
|
+
lines.push('Dashboard keeps running and will recover after the next valid update.');
|
|
26
|
+
} else {
|
|
27
|
+
lines.push('Fix the error and rerun dashboard.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return lines.map((line) => truncate(line, width));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
renderErrorLines
|
|
35
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function truncate(value, width) {
|
|
2
|
+
const text = String(value);
|
|
3
|
+
if (!Number.isFinite(width) || width <= 0 || text.length <= width) {
|
|
4
|
+
return text;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (width <= 3) {
|
|
8
|
+
return text.slice(0, width);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return `${text.slice(0, width - 3)}...`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatTime(value) {
|
|
15
|
+
if (!value) {
|
|
16
|
+
return 'n/a';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const date = new Date(value);
|
|
20
|
+
if (Number.isNaN(date.getTime())) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return date.toLocaleTimeString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderHeaderLines(params) {
|
|
28
|
+
const {
|
|
29
|
+
snapshot,
|
|
30
|
+
flow,
|
|
31
|
+
workspacePath,
|
|
32
|
+
view,
|
|
33
|
+
runFilter,
|
|
34
|
+
watchEnabled,
|
|
35
|
+
watchStatus,
|
|
36
|
+
lastRefreshAt,
|
|
37
|
+
width
|
|
38
|
+
} = params;
|
|
39
|
+
|
|
40
|
+
const projectName = snapshot?.project?.name || 'Unnamed FIRE project';
|
|
41
|
+
const topLine = `specsmd dashboard | ${flow.toUpperCase()} | ${projectName}`;
|
|
42
|
+
const subLine = [
|
|
43
|
+
`path: ${workspacePath}`,
|
|
44
|
+
`updated: ${formatTime(lastRefreshAt)}`,
|
|
45
|
+
`watch: ${watchEnabled ? watchStatus : 'off'}`,
|
|
46
|
+
`view: ${view}`,
|
|
47
|
+
`filter: ${runFilter}`
|
|
48
|
+
].join(' | ');
|
|
49
|
+
|
|
50
|
+
const horizontal = '-'.repeat(Math.max(20, Math.min(width || 120, 120)));
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
truncate(topLine, width),
|
|
54
|
+
truncate(subLine, width),
|
|
55
|
+
truncate(horizontal, width)
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
renderHeaderLines,
|
|
61
|
+
truncate
|
|
62
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { truncate } = require('./header');
|
|
2
|
+
|
|
3
|
+
function renderHelpLines(showHelp, width) {
|
|
4
|
+
if (!showHelp) {
|
|
5
|
+
return [truncate('Press h to show keyboard shortcuts.', width)];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return [
|
|
9
|
+
truncate('Keys: q quit | r refresh | h/? toggle help | tab cycle view | 1 runs | 2 overview | f cycle run filter', width)
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
renderHelpLines
|
|
15
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { truncate } = require('./header');
|
|
2
|
+
|
|
3
|
+
function safePercent(part, total) {
|
|
4
|
+
if (!total || total <= 0) {
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return Math.round((part / total) * 100);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function renderStatsLines(snapshot, width) {
|
|
12
|
+
if (!snapshot?.initialized) {
|
|
13
|
+
return [truncate('stats: waiting for .specs-fire/state.yaml initialization', width)];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stats = snapshot.stats;
|
|
17
|
+
const workItemProgress = `${stats.completedWorkItems}/${stats.totalWorkItems}`;
|
|
18
|
+
const workItemPct = safePercent(stats.completedWorkItems, stats.totalWorkItems);
|
|
19
|
+
|
|
20
|
+
const intentProgress = `${stats.completedIntents}/${stats.totalIntents}`;
|
|
21
|
+
const intentPct = safePercent(stats.completedIntents, stats.totalIntents);
|
|
22
|
+
|
|
23
|
+
const line = [
|
|
24
|
+
`Intents ${intentProgress} (${intentPct}%)`,
|
|
25
|
+
`Work items ${workItemProgress} (${workItemPct}%)`,
|
|
26
|
+
`Runs ${stats.activeRunsCount} active / ${stats.completedRuns} completed`,
|
|
27
|
+
`Blocked ${stats.blockedWorkItems}`
|
|
28
|
+
].join(' | ');
|
|
29
|
+
|
|
30
|
+
return [truncate(line, width)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
renderStatsLines
|
|
35
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { renderHeaderLines, truncate } = require('./components/header');
|
|
2
|
+
const { renderStatsLines } = require('./components/stats-strip');
|
|
3
|
+
const { renderErrorLines } = require('./components/error-banner');
|
|
4
|
+
const { renderHelpLines } = require('./components/help-footer');
|
|
5
|
+
const { renderRunsViewLines } = require('./views/runs-view');
|
|
6
|
+
const { renderOverviewViewLines } = require('./views/overview-view');
|
|
7
|
+
|
|
8
|
+
function normalizeWidth(width) {
|
|
9
|
+
if (!Number.isFinite(width)) {
|
|
10
|
+
return 120;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Math.max(40, Math.min(Math.floor(width), 180));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildDashboardLines(params) {
|
|
17
|
+
const {
|
|
18
|
+
snapshot,
|
|
19
|
+
error,
|
|
20
|
+
flow,
|
|
21
|
+
workspacePath,
|
|
22
|
+
view,
|
|
23
|
+
runFilter,
|
|
24
|
+
watchEnabled,
|
|
25
|
+
watchStatus,
|
|
26
|
+
showHelp,
|
|
27
|
+
lastRefreshAt,
|
|
28
|
+
width
|
|
29
|
+
} = params;
|
|
30
|
+
|
|
31
|
+
const safeWidth = normalizeWidth(width);
|
|
32
|
+
const lines = [];
|
|
33
|
+
|
|
34
|
+
lines.push(...renderHeaderLines({
|
|
35
|
+
snapshot,
|
|
36
|
+
flow,
|
|
37
|
+
workspacePath,
|
|
38
|
+
view,
|
|
39
|
+
runFilter,
|
|
40
|
+
watchEnabled,
|
|
41
|
+
watchStatus,
|
|
42
|
+
lastRefreshAt,
|
|
43
|
+
width: safeWidth
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
if (snapshot) {
|
|
47
|
+
lines.push(...renderStatsLines(snapshot, safeWidth));
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
lines.push(...renderErrorLines(error, safeWidth, watchEnabled));
|
|
53
|
+
lines.push('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!snapshot) {
|
|
57
|
+
lines.push(truncate('No snapshot available yet. Waiting for refresh...', safeWidth));
|
|
58
|
+
} else if (view === 'overview') {
|
|
59
|
+
lines.push(...renderOverviewViewLines(snapshot, safeWidth));
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(...renderRunsViewLines(snapshot, runFilter, safeWidth));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(...renderHelpLines(showHelp, safeWidth));
|
|
66
|
+
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatDashboardText(params) {
|
|
71
|
+
return buildDashboardLines(params).join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
buildDashboardLines,
|
|
76
|
+
formatDashboardText,
|
|
77
|
+
normalizeWidth
|
|
78
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function createInitialUIState() {
|
|
2
|
+
return {
|
|
3
|
+
view: 'runs',
|
|
4
|
+
runFilter: 'all',
|
|
5
|
+
showHelp: true
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cycleView(current) {
|
|
10
|
+
if (current === 'runs') {
|
|
11
|
+
return 'overview';
|
|
12
|
+
}
|
|
13
|
+
return 'runs';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cycleRunFilter(current) {
|
|
17
|
+
if (current === 'all') {
|
|
18
|
+
return 'active';
|
|
19
|
+
}
|
|
20
|
+
if (current === 'active') {
|
|
21
|
+
return 'completed';
|
|
22
|
+
}
|
|
23
|
+
return 'all';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
createInitialUIState,
|
|
28
|
+
cycleView,
|
|
29
|
+
cycleRunFilter
|
|
30
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { truncate } = require('../components/header');
|
|
2
|
+
|
|
3
|
+
const STANDARD_TYPES = [
|
|
4
|
+
'constitution',
|
|
5
|
+
'tech-stack',
|
|
6
|
+
'coding-standards',
|
|
7
|
+
'testing-standards',
|
|
8
|
+
'system-architecture'
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function renderOverviewViewLines(snapshot, width) {
|
|
12
|
+
const lines = ['Overview'];
|
|
13
|
+
|
|
14
|
+
if (!snapshot?.initialized) {
|
|
15
|
+
lines.push('FIRE project folder exists but state.yaml is missing.');
|
|
16
|
+
lines.push('Run initialization and the overview will appear automatically.');
|
|
17
|
+
return lines.map((line) => truncate(line, width));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const project = snapshot.project || {};
|
|
21
|
+
const workspace = snapshot.workspace || {};
|
|
22
|
+
|
|
23
|
+
lines.push(`Project: ${project.name || 'Unknown'} | FIRE version: ${project.fireVersion || snapshot.version || '0.0.0'}`);
|
|
24
|
+
lines.push(`Workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'} | autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`);
|
|
25
|
+
lines.push('');
|
|
26
|
+
|
|
27
|
+
lines.push('Intent Summary');
|
|
28
|
+
lines.push(` total: ${snapshot.stats.totalIntents} | completed: ${snapshot.stats.completedIntents} | in_progress: ${snapshot.stats.inProgressIntents} | pending: ${snapshot.stats.pendingIntents} | blocked: ${snapshot.stats.blockedIntents}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
|
|
31
|
+
lines.push('Work Item Summary');
|
|
32
|
+
lines.push(` total: ${snapshot.stats.totalWorkItems} | completed: ${snapshot.stats.completedWorkItems} | in_progress: ${snapshot.stats.inProgressWorkItems} | pending: ${snapshot.stats.pendingWorkItems} | blocked: ${snapshot.stats.blockedWorkItems}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
const standardSet = new Set((snapshot.standards || []).map((item) => item.type));
|
|
36
|
+
lines.push('Standards');
|
|
37
|
+
for (const type of STANDARD_TYPES) {
|
|
38
|
+
const marker = standardSet.has(type) ? '[x]' : '[ ]';
|
|
39
|
+
lines.push(` ${marker} ${type}.md`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('Top Intents');
|
|
44
|
+
|
|
45
|
+
const intents = (snapshot.intents || []).slice(0, 6);
|
|
46
|
+
if (intents.length === 0) {
|
|
47
|
+
lines.push(' - none');
|
|
48
|
+
} else {
|
|
49
|
+
for (const intent of intents) {
|
|
50
|
+
const totalWorkItems = (intent.workItems || []).length;
|
|
51
|
+
const completedWorkItems = (intent.workItems || []).filter((item) => item.status === 'completed').length;
|
|
52
|
+
lines.push(` - ${intent.id}: ${intent.status} (${completedWorkItems}/${totalWorkItems} work items completed)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.map((line) => truncate(line, width));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
renderOverviewViewLines
|
|
61
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { truncate } = require('../components/header');
|
|
2
|
+
|
|
3
|
+
function safeArray(value) {
|
|
4
|
+
return Array.isArray(value) ? value : [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function formatRunProgress(run) {
|
|
8
|
+
const workItems = safeArray(run.workItems);
|
|
9
|
+
const total = workItems.length;
|
|
10
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
11
|
+
const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
|
|
12
|
+
return `${completed}/${total} completed${inProgress > 0 ? `, ${inProgress} in_progress` : ''}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderActiveRunLines(activeRuns, width) {
|
|
16
|
+
const lines = ['Active Runs'];
|
|
17
|
+
if (!activeRuns || activeRuns.length === 0) {
|
|
18
|
+
lines.push(' - none');
|
|
19
|
+
return lines.map((line) => truncate(line, width));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const run of activeRuns) {
|
|
23
|
+
const currentItem = run.currentItem || 'n/a';
|
|
24
|
+
const artifacts = [
|
|
25
|
+
run.hasPlan ? 'plan' : null,
|
|
26
|
+
run.hasWalkthrough ? 'walkthrough' : null,
|
|
27
|
+
run.hasTestReport ? 'test-report' : null
|
|
28
|
+
].filter(Boolean).join(', ') || 'no artifacts yet';
|
|
29
|
+
|
|
30
|
+
lines.push(` - ${run.id} [${run.scope}] current: ${currentItem}`);
|
|
31
|
+
lines.push(` progress: ${formatRunProgress(run)} | artifacts: ${artifacts}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return lines.map((line) => truncate(line, width));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderPendingQueueLines(pendingItems, width) {
|
|
38
|
+
const lines = ['Pending Queue'];
|
|
39
|
+
if (!pendingItems || pendingItems.length === 0) {
|
|
40
|
+
lines.push(' - none');
|
|
41
|
+
return lines.map((line) => truncate(line, width));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const item of pendingItems.slice(0, 12)) {
|
|
45
|
+
const deps = item.dependencies && item.dependencies.length > 0
|
|
46
|
+
? ` deps:${item.dependencies.join(',')}`
|
|
47
|
+
: '';
|
|
48
|
+
lines.push(` - ${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (pendingItems.length > 12) {
|
|
52
|
+
lines.push(` ... ${pendingItems.length - 12} more pending items`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines.map((line) => truncate(line, width));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderCompletedRunLines(completedRuns, width) {
|
|
59
|
+
const lines = ['Recent Completed Runs'];
|
|
60
|
+
if (!completedRuns || completedRuns.length === 0) {
|
|
61
|
+
lines.push(' - none');
|
|
62
|
+
return lines.map((line) => truncate(line, width));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const run of completedRuns.slice(0, 5)) {
|
|
66
|
+
const completedAt = run.completedAt || 'unknown';
|
|
67
|
+
lines.push(` - ${run.id} [${run.scope}] completed: ${completedAt} | ${formatRunProgress(run)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines.map((line) => truncate(line, width));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderRunsViewLines(snapshot, runFilter, width) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
|
|
76
|
+
if (!snapshot?.initialized) {
|
|
77
|
+
lines.push('FIRE detected, but state.yaml is not initialized yet.');
|
|
78
|
+
lines.push('Initialize FIRE project context, then the dashboard will auto-populate.');
|
|
79
|
+
return lines.map((line) => truncate(line, width));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (runFilter !== 'completed') {
|
|
83
|
+
lines.push(...renderActiveRunLines(snapshot.activeRuns, width));
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(...renderPendingQueueLines(snapshot.pendingItems, width));
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (runFilter !== 'active') {
|
|
90
|
+
lines.push(...renderCompletedRunLines(snapshot.completedRuns, width));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lines.map((line) => truncate(line, width));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
renderRunsViewLines
|
|
98
|
+
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const CLIUtils = require('../cli-utils');
|
|
5
|
+
const { theme } = CLIUtils;
|
|
2
6
|
|
|
3
7
|
class CodexInstaller extends ToolInstaller {
|
|
4
8
|
get key() {
|
|
@@ -10,12 +14,79 @@ class CodexInstaller extends ToolInstaller {
|
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
get commandsDir() {
|
|
13
|
-
return '.codex';
|
|
17
|
+
return path.join('.codex', 'skills');
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
get detectPath() {
|
|
17
21
|
return '.codex';
|
|
18
22
|
}
|
|
23
|
+
|
|
24
|
+
async installCommands(flowPath, config) {
|
|
25
|
+
const targetSkillsDir = this.commandsDir;
|
|
26
|
+
console.log(theme.dim(` Installing skills to ${targetSkillsDir}/...`));
|
|
27
|
+
await fs.ensureDir(targetSkillsDir);
|
|
28
|
+
|
|
29
|
+
const commandsSourceDir = path.join(flowPath, 'commands');
|
|
30
|
+
if (!await fs.pathExists(commandsSourceDir)) {
|
|
31
|
+
console.log(theme.warning(` No commands folder found at ${commandsSourceDir}`));
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const commandFiles = await fs.readdir(commandsSourceDir);
|
|
36
|
+
const installedFiles = [];
|
|
37
|
+
|
|
38
|
+
for (const cmdFile of commandFiles) {
|
|
39
|
+
if (!cmdFile.endsWith('.md')) continue;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const sourcePath = path.join(commandsSourceDir, cmdFile);
|
|
43
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
44
|
+
const commandName = cmdFile.replace('.md', '');
|
|
45
|
+
const prefix = (config && config.command && config.command.prefix) ? `${config.command.prefix}-` : '';
|
|
46
|
+
const skillName = `specsmd-${prefix}${commandName}`;
|
|
47
|
+
|
|
48
|
+
const { description, body } = this.parseFrontmatter(content);
|
|
49
|
+
|
|
50
|
+
// Build SKILL.md with Codex frontmatter
|
|
51
|
+
const skillContent = [
|
|
52
|
+
'---',
|
|
53
|
+
`name: ${skillName}`,
|
|
54
|
+
`description: "${description || 'specsmd agent'}"`,
|
|
55
|
+
'---',
|
|
56
|
+
'',
|
|
57
|
+
body
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
// Write SKILL.md
|
|
61
|
+
const skillDir = path.join(targetSkillsDir, skillName);
|
|
62
|
+
await fs.ensureDir(skillDir);
|
|
63
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf8');
|
|
64
|
+
|
|
65
|
+
installedFiles.push(skillName);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.log(theme.warning(` Failed to install ${cmdFile}: ${err.message}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CLIUtils.displayStatus('', `Installed ${installedFiles.length} skills for ${this.name}`, 'success');
|
|
72
|
+
return installedFiles;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse YAML frontmatter from a markdown file
|
|
77
|
+
*/
|
|
78
|
+
parseFrontmatter(content) {
|
|
79
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
80
|
+
if (!match) return { description: '', body: content };
|
|
81
|
+
|
|
82
|
+
const frontmatter = match[1];
|
|
83
|
+
const body = match[2];
|
|
84
|
+
const descMatch = frontmatter.match(/description:\s*["']?(.+?)["']?\s*$/m);
|
|
85
|
+
return {
|
|
86
|
+
description: descMatch ? descMatch[1] : '',
|
|
87
|
+
body: body.trim()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
19
90
|
}
|
|
20
91
|
|
|
21
92
|
module.exports = CodexInstaller;
|