nubos-pilot 0.7.1 → 0.7.2
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/bin/np-tools/_commands.cjs +1 -1
- package/bin/np-tools/dashboard.cjs +11 -63
- package/lib/dashboard.cjs +43 -125
- package/lib/dashboard.test.cjs +28 -31
- package/package.json +1 -1
- package/workflows/dashboard.md +49 -0
|
@@ -63,7 +63,7 @@ const COMMANDS = [
|
|
|
63
63
|
{ name: 'worktree-remove', category: 'Execution', description: 'Remove a slice worktree + delete its branch (--force / --keep-branch)' },
|
|
64
64
|
{ name: 'worktree-list', category: 'Execution', description: 'List all nubos-pilot-managed slice worktrees (np/<mid>-<sid> only) as JSON' },
|
|
65
65
|
{ name: 'worktree-ff-merge', category: 'Execution', description: 'Fast-forward merge a slice branch back to its base (fails hard on non-FF)' },
|
|
66
|
-
{ name: 'dashboard', category: 'Utility', description: 'One-shot console dashboard of milestones
|
|
66
|
+
{ name: 'dashboard', category: 'Utility', description: 'One-shot console dashboard of milestones, slices, and tasks. Read-only; flags: --json, --no-color' },
|
|
67
67
|
{ name: 'thread-resume', category: 'Utility', description: 'Bump a thread markdown on resume (status OPEN→IN_PROGRESS, refresh last_resumed) via atomic write' },
|
|
68
68
|
{ name: 'state-incr', category: 'Capture', description: 'Increment a whitelisted STATE.md counter (e.g. pending_todos) under withFileLock' },
|
|
69
69
|
|
|
@@ -1,81 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const { collectSnapshot, renderSnapshot, ANSI } = require('../../lib/dashboard.cjs');
|
|
5
|
-
|
|
6
|
-
const MIN_WATCH_SECONDS = 1;
|
|
7
|
-
const MAX_WATCH_SECONDS = 3600;
|
|
3
|
+
const { collectSnapshot, renderSnapshot } = require('../../lib/dashboard.cjs');
|
|
8
4
|
|
|
9
5
|
function _parseArgs(args) {
|
|
10
|
-
const out = { json: false, noColor: false
|
|
11
|
-
for (
|
|
12
|
-
|
|
13
|
-
if (a === '--
|
|
14
|
-
if (a === '--no-color') { out.noColor = true; continue; }
|
|
15
|
-
if (a === '--watch') {
|
|
16
|
-
const raw = args[i + 1];
|
|
17
|
-
if (raw && !raw.startsWith('-')) { out.watch = Number(raw); i += 1; }
|
|
18
|
-
else out.watch = 3;
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
6
|
+
const out = { json: false, noColor: false };
|
|
7
|
+
for (const a of args) {
|
|
8
|
+
if (a === '--json') out.json = true;
|
|
9
|
+
else if (a === '--no-color') out.noColor = true;
|
|
21
10
|
}
|
|
22
11
|
return out;
|
|
23
12
|
}
|
|
24
13
|
|
|
25
|
-
function _renderOnce(cwd, stdout, parsed) {
|
|
26
|
-
const snap = collectSnapshot(cwd);
|
|
27
|
-
if (parsed.json) {
|
|
28
|
-
stdout.write(JSON.stringify(snap, null, 2) + '\n');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const useColor = !parsed.noColor && Boolean(stdout.isTTY);
|
|
32
|
-
stdout.write(renderSnapshot(snap, { color: useColor }) + '\n');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
14
|
function run(args, opts) {
|
|
36
15
|
const o = opts || {};
|
|
37
16
|
const cwd = o.cwd || process.cwd();
|
|
38
17
|
const stdout = o.stdout || process.stdout;
|
|
39
18
|
const parsed = _parseArgs(Array.isArray(args) ? args : []);
|
|
40
19
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'--watch seconds must be between ' + MIN_WATCH_SECONDS + ' and ' + MAX_WATCH_SECONDS,
|
|
46
|
-
{ got: parsed.watch },
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (parsed.json) {
|
|
50
|
-
throw new NubosPilotError(
|
|
51
|
-
'dashboard-watch-incompatible-json',
|
|
52
|
-
'--watch cannot be combined with --json (use a shell loop if you need JSON polling)',
|
|
53
|
-
{},
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
const tty = Boolean(stdout.isTTY);
|
|
57
|
-
const clear = tty ? ANSI.clearScreen : '';
|
|
58
|
-
const render = () => {
|
|
59
|
-
try {
|
|
60
|
-
stdout.write(clear);
|
|
61
|
-
_renderOnce(cwd, stdout, parsed);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
process.stderr.write('[nubos-pilot dashboard] render failed: ' + ((err && err.message) || err) + '\n');
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
render();
|
|
67
|
-
const handle = setInterval(render, parsed.watch * 1000);
|
|
68
|
-
const stop = () => {
|
|
69
|
-
clearInterval(handle);
|
|
70
|
-
try { if (tty) stdout.write('\x1b[?25h'); } catch {}
|
|
71
|
-
process.exit(0);
|
|
72
|
-
};
|
|
73
|
-
process.on('SIGINT', stop);
|
|
74
|
-
process.on('SIGTERM', stop);
|
|
75
|
-
return new Promise(() => { });
|
|
20
|
+
const snap = collectSnapshot(cwd);
|
|
21
|
+
if (parsed.json) {
|
|
22
|
+
stdout.write(JSON.stringify(snap, null, 2) + '\n');
|
|
23
|
+
return 0;
|
|
76
24
|
}
|
|
77
|
-
|
|
78
|
-
|
|
25
|
+
const useColor = !parsed.noColor && Boolean(stdout.isTTY);
|
|
26
|
+
stdout.write(renderSnapshot(snap, { color: useColor }) + '\n');
|
|
79
27
|
return 0;
|
|
80
28
|
}
|
|
81
29
|
|
package/lib/dashboard.cjs
CHANGED
|
@@ -4,11 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
7
|
-
const {
|
|
8
|
-
const { listMilestones, listSlices, listTasks, mId, sId } = require('./layout.cjs');
|
|
9
|
-
const { listHandoffs } = require('./handoff.cjs');
|
|
10
|
-
const { listSliceWorktrees, worktreeIsolationEnabled } = require('./worktree.cjs');
|
|
11
|
-
const { workspaceGitInfo } = require('./git.cjs');
|
|
7
|
+
const { listMilestones, listSlices, listTasks, mId } = require('./layout.cjs');
|
|
12
8
|
|
|
13
9
|
const ANSI = Object.freeze({
|
|
14
10
|
reset: '\x1b[0m',
|
|
@@ -20,7 +16,6 @@ const ANSI = Object.freeze({
|
|
|
20
16
|
blue: '\x1b[34m',
|
|
21
17
|
cyan: '\x1b[36m',
|
|
22
18
|
gray: '\x1b[90m',
|
|
23
|
-
clearScreen: '\x1b[2J\x1b[H',
|
|
24
19
|
});
|
|
25
20
|
|
|
26
21
|
const STATUS_GLYPHS = Object.freeze({
|
|
@@ -41,6 +36,15 @@ function _safeReadFile(p) {
|
|
|
41
36
|
catch { return null; }
|
|
42
37
|
}
|
|
43
38
|
|
|
39
|
+
function _taskStatus(planPath) {
|
|
40
|
+
const raw = _safeReadFile(planPath);
|
|
41
|
+
if (!raw) return 'pending';
|
|
42
|
+
try {
|
|
43
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
44
|
+
return typeof frontmatter.status === 'string' ? frontmatter.status : 'pending';
|
|
45
|
+
} catch { return 'pending'; }
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
function _collectMilestones(projectRoot) {
|
|
45
49
|
const out = [];
|
|
46
50
|
for (const m of listMilestones(projectRoot)) {
|
|
@@ -49,93 +53,34 @@ function _collectMilestones(projectRoot) {
|
|
|
49
53
|
for (const s of listSlices(m.number, projectRoot)) {
|
|
50
54
|
const tasks = listTasks(m.number, s.number, projectRoot);
|
|
51
55
|
const counts = { total: 0, pending: 0, 'in-progress': 0, done: 0, skipped: 0, parked: 0 };
|
|
56
|
+
const statuses = [];
|
|
52
57
|
for (const t of tasks) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
let fm;
|
|
56
|
-
try { ({ frontmatter: fm } = extractFrontmatter(raw)); } catch { fm = {}; }
|
|
57
|
-
const status = typeof fm.status === 'string' ? fm.status : 'pending';
|
|
58
|
+
const status = _taskStatus(t.plan_path);
|
|
59
|
+
statuses.push(status);
|
|
58
60
|
counts.total += 1;
|
|
59
61
|
if (Object.prototype.hasOwnProperty.call(counts, status)) counts[status] += 1;
|
|
60
62
|
}
|
|
61
63
|
slices.push({
|
|
62
64
|
id: s.id,
|
|
63
65
|
full_id: s.full_id,
|
|
64
|
-
path: s.path,
|
|
65
66
|
counts,
|
|
66
|
-
|
|
67
|
-
const raw = _safeReadFile(t.plan_path);
|
|
68
|
-
if (!raw) return 'pending';
|
|
69
|
-
try { return extractFrontmatter(raw).frontmatter.status || 'pending'; }
|
|
70
|
-
catch { return 'pending'; }
|
|
71
|
-
}),
|
|
67
|
+
task_statuses: statuses,
|
|
72
68
|
});
|
|
73
69
|
}
|
|
74
70
|
out.push({
|
|
75
71
|
id: m.id,
|
|
76
72
|
number: m.number,
|
|
77
|
-
name:
|
|
78
|
-
status:
|
|
73
|
+
name: typeof meta.name === 'string' ? meta.name : null,
|
|
74
|
+
status: typeof meta.status === 'string' ? meta.status : null,
|
|
79
75
|
slices,
|
|
80
76
|
});
|
|
81
77
|
}
|
|
82
78
|
return out;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
function _readState(projectRoot) {
|
|
86
|
-
const p = path.join(projectStateDir(projectRoot), 'STATE.md');
|
|
87
|
-
const raw = _safeReadFile(p);
|
|
88
|
-
if (!raw) return { current_milestone: null, current_task: null };
|
|
89
|
-
try {
|
|
90
|
-
const { frontmatter } = extractFrontmatter(raw);
|
|
91
|
-
return {
|
|
92
|
-
current_milestone: frontmatter.current_milestone || null,
|
|
93
|
-
current_task: frontmatter.current_task || null,
|
|
94
|
-
};
|
|
95
|
-
} catch {
|
|
96
|
-
return { current_milestone: null, current_task: null };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
81
|
function collectSnapshot(projectRoot) {
|
|
101
82
|
const cwd = projectRoot || process.cwd();
|
|
102
|
-
|
|
103
|
-
const state = _readState(cwd);
|
|
104
|
-
const milestones = _collectMilestones(cwd);
|
|
105
|
-
const worktrees = (() => { try { return listSliceWorktrees(cwd); } catch { return []; } })();
|
|
106
|
-
const handoffs = (() => {
|
|
107
|
-
try { return listHandoffs({}, cwd); } catch { return []; }
|
|
108
|
-
})();
|
|
109
|
-
const openHandoffs = handoffs.filter((h) => h.status === 'open');
|
|
110
|
-
return {
|
|
111
|
-
generated_at: new Date().toISOString(),
|
|
112
|
-
project_root: cwd,
|
|
113
|
-
git,
|
|
114
|
-
state,
|
|
115
|
-
milestones,
|
|
116
|
-
worktrees,
|
|
117
|
-
handoffs: {
|
|
118
|
-
total: handoffs.length,
|
|
119
|
-
open: openHandoffs.length,
|
|
120
|
-
recent: handoffs.slice(-5).reverse(),
|
|
121
|
-
},
|
|
122
|
-
worktree_isolation: worktreeIsolationEnabled(cwd),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function _pad(s, n) {
|
|
127
|
-
const str = String(s);
|
|
128
|
-
if (str.length >= n) return str;
|
|
129
|
-
return str + ' '.repeat(n - str.length);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function _sliceBar(statuses, useColor) {
|
|
133
|
-
const parts = [];
|
|
134
|
-
for (const s of statuses) {
|
|
135
|
-
const g = STATUS_GLYPHS[s] || STATUS_GLYPHS.pending;
|
|
136
|
-
parts.push(useColor ? g.color + g.glyph + ANSI.reset : g.glyph);
|
|
137
|
-
}
|
|
138
|
-
return parts.join(' ');
|
|
83
|
+
return { milestones: _collectMilestones(cwd) };
|
|
139
84
|
}
|
|
140
85
|
|
|
141
86
|
function _summarizeCounts(c, useColor) {
|
|
@@ -149,73 +94,46 @@ function _summarizeCounts(c, useColor) {
|
|
|
149
94
|
return bits.join(' · ') || paint(ANSI.dim, 'no tasks');
|
|
150
95
|
}
|
|
151
96
|
|
|
97
|
+
function _checkboxRow(statuses, useColor) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const s of statuses) {
|
|
100
|
+
const g = STATUS_GLYPHS[s] || STATUS_GLYPHS.pending;
|
|
101
|
+
parts.push(useColor ? g.color + g.glyph + ANSI.reset : g.glyph);
|
|
102
|
+
}
|
|
103
|
+
return parts.join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
152
106
|
function renderSnapshot(snap, opts) {
|
|
153
107
|
const o = opts || {};
|
|
154
108
|
const useColor = o.color !== false;
|
|
155
|
-
const lines = [];
|
|
156
109
|
const c = (code, text) => useColor ? code + text + ANSI.reset : text;
|
|
110
|
+
const lines = [];
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
const branch = (snap.git && snap.git.current_branch) ? snap.git.current_branch : '(no git)';
|
|
160
|
-
lines.push(c(ANSI.bold + ANSI.blue, title) + ' ' + c(ANSI.dim, snap.project_root));
|
|
161
|
-
lines.push(c(ANSI.dim, 'Branch: ' + branch + ' · Generated: ' + snap.generated_at));
|
|
162
|
-
if (snap.worktree_isolation) {
|
|
163
|
-
lines.push(c(ANSI.cyan, 'Worktree isolation: on') + c(ANSI.dim, ' (' + snap.worktrees.length + ' active)'));
|
|
164
|
-
} else {
|
|
165
|
-
lines.push(c(ANSI.dim, 'Worktree isolation: off'));
|
|
166
|
-
}
|
|
112
|
+
lines.push(c(ANSI.bold + ANSI.blue, 'nubos-pilot'));
|
|
167
113
|
lines.push('');
|
|
168
114
|
|
|
169
|
-
if (snap.milestones.length === 0) {
|
|
115
|
+
if (!snap.milestones || snap.milestones.length === 0) {
|
|
170
116
|
lines.push(c(ANSI.dim, 'No milestones yet. Run /np:new-project or /np:new-milestone.'));
|
|
171
|
-
} else {
|
|
172
|
-
lines.push(c(ANSI.bold, 'Milestones'));
|
|
173
|
-
for (const m of snap.milestones) {
|
|
174
|
-
const name = m.name ? ' — ' + m.name : '';
|
|
175
|
-
const status = m.status ? ' ' + c(ANSI.dim, '[' + m.status + ']') : '';
|
|
176
|
-
const marker = (snap.state.current_milestone === m.id) ? c(ANSI.cyan, '▶ ') : ' ';
|
|
177
|
-
lines.push(marker + c(ANSI.bold, m.id) + name + status);
|
|
178
|
-
if (m.slices.length === 0) {
|
|
179
|
-
lines.push(' ' + c(ANSI.dim, 'no slices planned'));
|
|
180
|
-
}
|
|
181
|
-
for (const s of m.slices) {
|
|
182
|
-
lines.push(' ' + c(ANSI.bold, s.full_id) + ' ' + _summarizeCounts(s.counts, useColor));
|
|
183
|
-
if (s.tasks_statuses.length > 0) {
|
|
184
|
-
lines.push(' ' + _sliceBar(s.tasks_statuses, useColor));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
lines.push('');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (snap.worktrees.length > 0) {
|
|
192
|
-
lines.push(c(ANSI.bold, 'Active worktrees'));
|
|
193
|
-
for (const w of snap.worktrees) {
|
|
194
|
-
lines.push(' ' + c(ANSI.cyan, w.slice_full_id) + ' ' + c(ANSI.dim, w.path));
|
|
195
|
-
}
|
|
196
117
|
lines.push('');
|
|
118
|
+
return lines.join('\n');
|
|
197
119
|
}
|
|
198
120
|
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const route = c(ANSI.cyan, _pad(h.from_agent + ' → ' + h.to_agent, 32));
|
|
212
|
-
const ms = h.milestone ? c(ANSI.dim, ' [' + h.milestone + ']') : '';
|
|
213
|
-
lines.push(' ' + status + ' ' + route + ' ' + h.topic + ms);
|
|
121
|
+
for (const m of snap.milestones) {
|
|
122
|
+
const name = m.name ? ' — ' + m.name : '';
|
|
123
|
+
const status = m.status ? ' ' + c(ANSI.dim, '[' + m.status + ']') : '';
|
|
124
|
+
lines.push(c(ANSI.bold, m.id) + name + status);
|
|
125
|
+
if (m.slices.length === 0) {
|
|
126
|
+
lines.push(' ' + c(ANSI.dim, 'no slices planned'));
|
|
127
|
+
}
|
|
128
|
+
for (const s of m.slices) {
|
|
129
|
+
lines.push(' ' + c(ANSI.bold, s.full_id) + ' ' + _summarizeCounts(s.counts, useColor));
|
|
130
|
+
if (s.task_statuses.length > 0) {
|
|
131
|
+
lines.push(' ' + _checkboxRow(s.task_statuses, useColor));
|
|
132
|
+
}
|
|
214
133
|
}
|
|
134
|
+
lines.push('');
|
|
215
135
|
}
|
|
216
|
-
lines.push('');
|
|
217
136
|
|
|
218
|
-
lines.push(c(ANSI.dim, 'Refresh: re-run `np-tools.cjs dashboard` (or --watch <seconds>).'));
|
|
219
137
|
return lines.join('\n');
|
|
220
138
|
}
|
|
221
139
|
|
package/lib/dashboard.test.cjs
CHANGED
|
@@ -50,14 +50,11 @@ function _writeMeta(root, mNum, meta) {
|
|
|
50
50
|
fs.writeFileSync(path.join(dir, mIdStr + '-META.json'), JSON.stringify(meta), 'utf-8');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
test('DB-1: collectSnapshot returns
|
|
53
|
+
test('DB-1: collectSnapshot returns only { milestones } shape', () => {
|
|
54
54
|
const root = _sandbox();
|
|
55
55
|
try {
|
|
56
56
|
const snap = dashboard.collectSnapshot(root);
|
|
57
|
-
|
|
58
|
-
assert.ok(key in snap, 'missing key: ' + key);
|
|
59
|
-
}
|
|
60
|
-
assert.equal(snap.project_root, root);
|
|
57
|
+
assert.deepEqual(Object.keys(snap), ['milestones']);
|
|
61
58
|
assert.equal(Array.isArray(snap.milestones), true);
|
|
62
59
|
} finally {
|
|
63
60
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -82,12 +79,13 @@ test('DB-2: collectSnapshot counts task statuses per slice', () => {
|
|
|
82
79
|
assert.deepEqual(m.slices[0].counts, {
|
|
83
80
|
total: 5, pending: 1, 'in-progress': 1, done: 2, skipped: 1, parked: 0,
|
|
84
81
|
});
|
|
82
|
+
assert.deepEqual(m.slices[0].task_statuses, ['done', 'done', 'in-progress', 'pending', 'skipped']);
|
|
85
83
|
} finally {
|
|
86
84
|
fs.rmSync(root, { recursive: true, force: true });
|
|
87
85
|
}
|
|
88
86
|
});
|
|
89
87
|
|
|
90
|
-
test('DB-3: renderSnapshot
|
|
88
|
+
test('DB-3: renderSnapshot prints milestone, slice, checkbox row', () => {
|
|
91
89
|
const root = _sandbox();
|
|
92
90
|
try {
|
|
93
91
|
_writeMeta(root, 1, { name: 'Auth', status: 'active' });
|
|
@@ -95,12 +93,13 @@ test('DB-3: renderSnapshot produces a non-empty string with key headings', () =>
|
|
|
95
93
|
_writeTask(root, 1, 1, 2, 'pending', 'Logout');
|
|
96
94
|
const snap = dashboard.collectSnapshot(root);
|
|
97
95
|
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
98
|
-
assert.match(out,
|
|
99
|
-
assert.match(out, /
|
|
100
|
-
assert.match(out, /
|
|
101
|
-
assert.match(out, /
|
|
102
|
-
assert.match(out, /
|
|
103
|
-
assert.match(out, /
|
|
96
|
+
assert.match(out, /^nubos-pilot/);
|
|
97
|
+
assert.match(out, /M001 — Auth/);
|
|
98
|
+
assert.match(out, /\[active\]/);
|
|
99
|
+
assert.match(out, /M001-S001/);
|
|
100
|
+
assert.match(out, /1 done/);
|
|
101
|
+
assert.match(out, /1 pending/);
|
|
102
|
+
assert.match(out, /\[x\] \[ \]/);
|
|
104
103
|
} finally {
|
|
105
104
|
fs.rmSync(root, { recursive: true, force: true });
|
|
106
105
|
}
|
|
@@ -117,15 +116,14 @@ test('DB-4: renderSnapshot shows "No milestones yet" when none exist', () => {
|
|
|
117
116
|
}
|
|
118
117
|
});
|
|
119
118
|
|
|
120
|
-
test('DB-5: renderSnapshot with color=false emits no ANSI
|
|
119
|
+
test('DB-5: renderSnapshot with color=false emits no ANSI codes', () => {
|
|
121
120
|
const root = _sandbox();
|
|
122
121
|
try {
|
|
123
122
|
_writeMeta(root, 1, { name: 'Auth' });
|
|
124
123
|
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
125
124
|
const snap = dashboard.collectSnapshot(root);
|
|
126
125
|
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
127
|
-
|
|
128
|
-
assert.equal(seen, false, 'renderSnapshot with color=false must not emit ANSI codes');
|
|
126
|
+
assert.equal(/\x1b\[/.test(out), false, 'render with color=false must not emit ANSI');
|
|
129
127
|
} finally {
|
|
130
128
|
fs.rmSync(root, { recursive: true, force: true });
|
|
131
129
|
}
|
|
@@ -150,32 +148,31 @@ test('DB-7: STATUS_GLYPHS covers all task-status enum values', () => {
|
|
|
150
148
|
}
|
|
151
149
|
});
|
|
152
150
|
|
|
153
|
-
test('DB-8:
|
|
151
|
+
test('DB-8: empty slice (no tasks) renders "no tasks" indicator', () => {
|
|
154
152
|
const root = _sandbox();
|
|
155
153
|
try {
|
|
154
|
+
_writeMeta(root, 1, { name: 'Empty' });
|
|
155
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks'), { recursive: true });
|
|
156
156
|
const snap = dashboard.collectSnapshot(root);
|
|
157
|
-
|
|
158
|
-
assert.
|
|
159
|
-
assert.
|
|
157
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
158
|
+
assert.match(out, /M001-S001/);
|
|
159
|
+
assert.match(out, /no tasks/);
|
|
160
160
|
} finally {
|
|
161
161
|
fs.rmSync(root, { recursive: true, force: true });
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
test('DB-9:
|
|
165
|
+
test('DB-9: multiple milestones render in numeric order', () => {
|
|
166
166
|
const root = _sandbox();
|
|
167
167
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
176
|
-
const snap2 = dashboard.collectSnapshot(root);
|
|
177
|
-
const out2 = dashboard.renderSnapshot(snap2, { color: false });
|
|
178
|
-
assert.match(out2, /Worktree isolation: on/);
|
|
168
|
+
_writeMeta(root, 2, { name: 'Second' });
|
|
169
|
+
_writeMeta(root, 1, { name: 'First' });
|
|
170
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
171
|
+
_writeTask(root, 2, 1, 1, 'pending', 'Y');
|
|
172
|
+
const snap = dashboard.collectSnapshot(root);
|
|
173
|
+
assert.equal(snap.milestones.length, 2);
|
|
174
|
+
assert.equal(snap.milestones[0].id, 'M001');
|
|
175
|
+
assert.equal(snap.milestones[1].id, 'M002');
|
|
179
176
|
} finally {
|
|
180
177
|
fs.rmSync(root, { recursive: true, force: true });
|
|
181
178
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
command: np:dashboard
|
|
3
|
+
description: One-shot console dashboard of milestones, slices, and tasks. Read-only — no files written, no state mutated, no git commit.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# np:dashboard
|
|
7
|
+
|
|
8
|
+
Read-only snapshot of milestones, slices, and task statuses for the project.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node .nubos-pilot/bin/np-tools.cjs dashboard
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That is the entire workflow. The CLI prints the snapshot to stdout — milestone-by-milestone, with one row per slice showing per-status counts plus a checkbox row of all tasks in the slice (`[ ]` pending, `[~]` in-progress, `[x]` done, `[-]` skipped, `[!]` parked).
|
|
15
|
+
|
|
16
|
+
## No Commit
|
|
17
|
+
|
|
18
|
+
Read-only. No files are written, no state is mutated, no git commit is made.
|
|
19
|
+
|
|
20
|
+
## Scope Guardrail
|
|
21
|
+
|
|
22
|
+
<scope_guardrail>
|
|
23
|
+
**Do:**
|
|
24
|
+
- Run the CLI with no arguments for the formatted view, `--json` for the raw snapshot, or `--no-color` for plain text.
|
|
25
|
+
- Treat the output as a render — re-run the workflow when you want a fresh view.
|
|
26
|
+
|
|
27
|
+
**Don't:**
|
|
28
|
+
- Add a long-running watch loop here — single-shot only, by design (ADR-0001).
|
|
29
|
+
- Mutate any state from this workflow — strictly read-only.
|
|
30
|
+
- Add additional sections beyond milestones / slices / tasks. Drill-down into handoffs / checkpoints / worktrees uses their dedicated commands.
|
|
31
|
+
</scope_guardrail>
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
|
|
35
|
+
Stdout only — formatted milestone overview with checkbox rows per slice. No files created. No state mutated. No git commit.
|
|
36
|
+
|
|
37
|
+
## Success Criteria
|
|
38
|
+
|
|
39
|
+
- [ ] Empty project renders the "No milestones yet" placeholder line.
|
|
40
|
+
- [ ] Every milestone in `roadmap.yaml` appears with its name and status.
|
|
41
|
+
- [ ] Every slice's checkbox row reflects current task frontmatter `status` values.
|
|
42
|
+
- [ ] `--json` emits the snapshot shape `{ milestones: [{ id, number, name, status, slices: [{ id, full_id, counts, task_statuses }] }] }`.
|
|
43
|
+
- [ ] `--no-color` emits no ANSI escape sequences.
|
|
44
|
+
- [ ] Zero file writes, zero state mutations, zero commits.
|
|
45
|
+
|
|
46
|
+
## Related Workflows
|
|
47
|
+
|
|
48
|
+
- **`/np:stats`** — phases-table + metrics aggregation (commit-history view).
|
|
49
|
+
- **`/np:state`** — current STATE.md frontmatter snapshot.
|