@doquflow/cli 1.5.2 → 1.7.0
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/dist/commands/help.js +37 -0
- package/dist/commands/ingest.js +115 -0
- package/dist/commands/init.js +44 -42
- package/dist/commands/query.js +141 -0
- package/dist/commands/recent.js +72 -205
- package/dist/commands/rewiki.js +339 -0
- package/dist/index.js +147 -141
- package/package.json +2 -2
- package/ui-dist/assets/index-B20T-7YT.js +44 -0
- package/ui-dist/assets/index-Cnq2PhDd.js +44 -0
- package/ui-dist/index.html +1 -1
package/dist/commands/recent.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* docuflow recent
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Shows a dashboard of recent activity:
|
|
6
|
+
* - Recent git commits (last N days)
|
|
7
|
+
* - Recent wiki activity from .docuflow/log.md
|
|
8
8
|
*/
|
|
9
9
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
10
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -14,106 +14,27 @@ exports.run = run;
|
|
|
14
14
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
15
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
16
|
const node_child_process_1 = require("node:child_process");
|
|
17
|
-
// ── ANSI helpers
|
|
17
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
18
18
|
const c = {
|
|
19
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
20
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
21
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
22
19
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
23
20
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
21
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
24
22
|
};
|
|
25
|
-
function colourStatus(status, text) {
|
|
26
|
-
if (status === "approved")
|
|
27
|
-
return c.green(text);
|
|
28
|
-
if (status === "needs-work")
|
|
29
|
-
return c.yellow(text);
|
|
30
|
-
if (status === "rejected")
|
|
31
|
-
return c.red(text);
|
|
32
|
-
return c.dim(text);
|
|
33
|
-
}
|
|
34
|
-
function normaliseStatus(raw) {
|
|
35
|
-
// Strip emojis (✅ ⚠️ ⏳ ❌) and other non-printable/non-ASCII characters
|
|
36
|
-
const stripped = raw.replace(/[^\x20-\x7E]/g, " ").trim().toLowerCase();
|
|
37
|
-
if (stripped.includes("approved"))
|
|
38
|
-
return "approved";
|
|
39
|
-
if (stripped.includes("needs-work") || stripped.includes("needs_work"))
|
|
40
|
-
return "needs-work";
|
|
41
|
-
if (stripped.includes("rejected"))
|
|
42
|
-
return "rejected";
|
|
43
|
-
if (stripped.includes("pending"))
|
|
44
|
-
return "pending";
|
|
45
|
-
return stripped.split(/\s+/)[0] || "pending";
|
|
46
|
-
}
|
|
47
|
-
function truncate(s, max) {
|
|
48
|
-
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
49
|
-
}
|
|
50
23
|
function formatDateYMD(d) {
|
|
51
24
|
return d.toISOString().slice(0, 10);
|
|
52
25
|
}
|
|
53
|
-
// ── Git
|
|
54
|
-
function
|
|
26
|
+
// ── Git commits (recent, date-filtered) ──────────────────────────────────────
|
|
27
|
+
function recentCommits(days) {
|
|
55
28
|
try {
|
|
56
|
-
const out = (0, node_child_process_1.execSync)(`git log --oneline --
|
|
29
|
+
const out = (0, node_child_process_1.execSync)(`git log --oneline --since="${days} days ago"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
57
30
|
return out.trim().split("\n").filter(Boolean);
|
|
58
31
|
}
|
|
59
32
|
catch {
|
|
60
33
|
return [];
|
|
61
34
|
}
|
|
62
35
|
}
|
|
63
|
-
// ──
|
|
64
|
-
|
|
65
|
-
let content;
|
|
66
|
-
try {
|
|
67
|
-
content = await promises_1.default.readFile(specPath, "utf8");
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return {};
|
|
71
|
-
}
|
|
72
|
-
let title = "";
|
|
73
|
-
let feature = "";
|
|
74
|
-
let status = "pending";
|
|
75
|
-
for (const line of content.split("\n")) {
|
|
76
|
-
// First H1 heading: "# TASK-YYYYMMDD-HHMMSS: <title>"
|
|
77
|
-
if (!title && line.startsWith("# TASK-")) {
|
|
78
|
-
const colonIdx = line.indexOf(":");
|
|
79
|
-
if (colonIdx !== -1) {
|
|
80
|
-
title = line.slice(colonIdx + 1).trim();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// **Feature**: value
|
|
84
|
-
if (!feature && line.startsWith("**Feature**:")) {
|
|
85
|
-
feature = line.slice("**Feature**:".length).trim().slice(0, 120).trimEnd();
|
|
86
|
-
}
|
|
87
|
-
// **Status**: value
|
|
88
|
-
if (line.startsWith("**Status**:")) {
|
|
89
|
-
const raw = line.slice("**Status**:".length).trim();
|
|
90
|
-
status = normaliseStatus(raw);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { title, feature, status };
|
|
94
|
-
}
|
|
95
|
-
// ── Review parser ────────────────────────────────────────────────────────────
|
|
96
|
-
async function parseReview(reviewPath) {
|
|
97
|
-
let content;
|
|
98
|
-
try {
|
|
99
|
-
content = await promises_1.default.readFile(reviewPath, "utf8");
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return { score: null, reviewVerdict: null };
|
|
103
|
-
}
|
|
104
|
-
let score = null;
|
|
105
|
-
let reviewVerdict = null;
|
|
106
|
-
const scoreMatch = content.match(/Score:\s*(\d+\/\d+)/);
|
|
107
|
-
if (scoreMatch)
|
|
108
|
-
score = scoreMatch[1];
|
|
109
|
-
const verdictMatch = content.match(/Verdict:\s*(APPROVED|NEEDS[\s_]WORK|REJECTED)/i);
|
|
110
|
-
if (verdictMatch) {
|
|
111
|
-
reviewVerdict = verdictMatch[1].toUpperCase().replace(/[\s-]+/g, "_");
|
|
112
|
-
}
|
|
113
|
-
return { score, reviewVerdict };
|
|
114
|
-
}
|
|
115
|
-
// ── Wiki log parser ──────────────────────────────────────────────────────────
|
|
116
|
-
// log.md format: ## [2026-05-01T13:49:38.141Z] operation | description
|
|
36
|
+
// ── Wiki log parsing ──────────────────────────────────────────────────────────
|
|
37
|
+
const headingRe = /^##\s+\[([^\]]+)\]\s+([^|]+?)(?:\s*\|\s*(.+))?$/;
|
|
117
38
|
async function parseWikiLog(logPath, since) {
|
|
118
39
|
let content;
|
|
119
40
|
try {
|
|
@@ -123,73 +44,80 @@ async function parseWikiLog(logPath, since) {
|
|
|
123
44
|
return [];
|
|
124
45
|
}
|
|
125
46
|
const entries = [];
|
|
126
|
-
const headingRe = /^##\s+\[([^\]]+)\]\s+([^|]+)\|\s*(.+)$/;
|
|
127
47
|
for (const line of content.split("\n")) {
|
|
128
|
-
const m = line.
|
|
129
|
-
if (!m)
|
|
48
|
+
const m = headingRe.exec(line.trim());
|
|
49
|
+
if (!m) {
|
|
50
|
+
// Legacy pipe-delimited: timestamp | tool | target | delta
|
|
51
|
+
const parts = line.split("|").map(p => p.trim());
|
|
52
|
+
if (parts.length >= 3 && parts[0].includes("T")) {
|
|
53
|
+
const d = new Date(parts[0]);
|
|
54
|
+
if (!isNaN(d.getTime()) && d >= since) {
|
|
55
|
+
entries.push({ date: formatDateYMD(d), operation: parts[1] ?? "", file: parts[2] ?? "" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
130
58
|
continue;
|
|
131
|
-
const rawDate = m[1].trim();
|
|
132
|
-
const operation = m[2].trim();
|
|
133
|
-
const file = m[3].trim();
|
|
134
|
-
const dateVal = new Date(rawDate);
|
|
135
|
-
if (!isNaN(dateVal.getTime()) && dateVal >= since) {
|
|
136
|
-
entries.push({ date: rawDate.slice(0, 10), operation, file });
|
|
137
59
|
}
|
|
60
|
+
const d = new Date(m[1]);
|
|
61
|
+
if (isNaN(d.getTime()) || d < since)
|
|
62
|
+
continue;
|
|
63
|
+
entries.push({ date: formatDateYMD(d), operation: (m[2] ?? "").trim(), file: (m[3] ?? "").trim() });
|
|
138
64
|
}
|
|
139
65
|
return entries;
|
|
140
66
|
}
|
|
141
|
-
// ── Renderers
|
|
142
|
-
function renderTable(
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
console.log(
|
|
146
|
-
console.log(` ${
|
|
147
|
-
console.log(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const row = ` ${t.id.padEnd(24)} ${title} ${status} ${score}`;
|
|
153
|
-
console.log(colourStatus(t.status, row));
|
|
67
|
+
// ── Renderers ─────────────────────────────────────────────────────────────────
|
|
68
|
+
function renderTable(commits, wikiEntries, days) {
|
|
69
|
+
const fromDate = formatDateYMD(new Date(Date.now() - days * 86400000));
|
|
70
|
+
const toDate = formatDateYMD(new Date());
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log(c.bold(`DocuFlow — Recent Activity (${fromDate} → ${toDate})`));
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log(c.bold(`Git Commits (last ${days} days)`));
|
|
75
|
+
console.log("─".repeat(60));
|
|
76
|
+
if (commits.length === 0) {
|
|
77
|
+
console.log(c.dim(" No commits in this period."));
|
|
154
78
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
console.log(` ${line}`);
|
|
79
|
+
else {
|
|
80
|
+
for (const line of commits) {
|
|
81
|
+
const sha = line.slice(0, 7);
|
|
82
|
+
const msg = line.slice(8);
|
|
83
|
+
console.log(` ${c.cyan(sha)} ${msg}`);
|
|
161
84
|
}
|
|
162
85
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(c.bold(`Wiki Activity (last ${days} days)`));
|
|
88
|
+
console.log("─".repeat(60));
|
|
89
|
+
if (wikiEntries.length === 0) {
|
|
90
|
+
console.log(c.dim(" No wiki activity in this period."));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
166
93
|
for (const e of wikiEntries) {
|
|
167
|
-
console.log(` ${e.date} ${e.operation}
|
|
94
|
+
console.log(` ${c.dim(e.date)} ${e.operation}${e.file ? " " + c.dim(e.file) : ""}`);
|
|
168
95
|
}
|
|
169
96
|
}
|
|
97
|
+
console.log("");
|
|
170
98
|
}
|
|
171
|
-
function renderMarkdown(
|
|
172
|
-
|
|
99
|
+
function renderMarkdown(commits, wikiEntries, days) {
|
|
100
|
+
const fromDate = formatDateYMD(new Date(Date.now() - days * 86400000));
|
|
101
|
+
const toDate = formatDateYMD(new Date());
|
|
102
|
+
console.log(`# DocuFlow — Recent Activity (${fromDate} → ${toDate})`);
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(`## Git Commits (last ${days} days)`);
|
|
173
105
|
console.log("");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const t of tasks) {
|
|
177
|
-
const title = truncate(t.title || t.feature, 40);
|
|
178
|
-
const score = t.score ?? "—";
|
|
179
|
-
console.log(`| ${t.id} | ${title} | ${t.status} | ${score} |`);
|
|
106
|
+
if (commits.length === 0) {
|
|
107
|
+
console.log("_No commits in this period._");
|
|
180
108
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.log(`## Git Commits (${allCommits.length} matching)`);
|
|
184
|
-
console.log("");
|
|
185
|
-
for (const line of allCommits) {
|
|
109
|
+
else {
|
|
110
|
+
for (const line of commits) {
|
|
186
111
|
console.log(`- ${line}`);
|
|
187
112
|
}
|
|
188
113
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log(`## Wiki Activity (last ${days} days)`);
|
|
116
|
+
console.log("");
|
|
117
|
+
if (wikiEntries.length === 0) {
|
|
118
|
+
console.log("_No wiki activity in this period._");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
193
121
|
console.log("| Date | Operation | File |");
|
|
194
122
|
console.log("|------|-----------|------|");
|
|
195
123
|
for (const e of wikiEntries) {
|
|
@@ -197,80 +125,19 @@ function renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate)
|
|
|
197
125
|
}
|
|
198
126
|
}
|
|
199
127
|
}
|
|
200
|
-
// ── Main entry point
|
|
128
|
+
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
201
129
|
async function run(opts = { days: 7, format: "table" }) {
|
|
202
130
|
const days = Math.max(1, isNaN(opts.days) ? 7 : opts.days);
|
|
203
131
|
const format = opts.format ?? "table";
|
|
204
|
-
const isMd = format === "md";
|
|
205
132
|
const cwd = process.cwd();
|
|
206
|
-
const specsDir = node_path_1.default.join(cwd, ".devloop", "specs");
|
|
207
133
|
const logPath = node_path_1.default.join(cwd, ".docuflow", "log.md");
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
// Collect spec files
|
|
211
|
-
let specFiles = [];
|
|
212
|
-
try {
|
|
213
|
-
const entries = await promises_1.default.readdir(specsDir);
|
|
214
|
-
specFiles = entries
|
|
215
|
-
.filter(f => /^TASK-\d{8}-\d{6}\.md$/.test(f))
|
|
216
|
-
.map(f => node_path_1.default.join(specsDir, f));
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
console.log(`No tasks found in the last ${days} days.`);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
// Filter by mtime and parse each spec
|
|
223
|
-
const tasks = [];
|
|
224
|
-
for (const specPath of specFiles) {
|
|
225
|
-
let stat;
|
|
226
|
-
try {
|
|
227
|
-
stat = await promises_1.default.stat(specPath);
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (stat.mtime < since)
|
|
233
|
-
continue;
|
|
234
|
-
const stem = node_path_1.default.basename(specPath, ".md"); // TASK-YYYYMMDD-HHMMSS
|
|
235
|
-
const specFields = await parseSpec(specPath);
|
|
236
|
-
if (!specFields.title && !specFields.feature)
|
|
237
|
-
continue; // unreadable
|
|
238
|
-
const reviewPath = node_path_1.default.join(specsDir, `${stem}-review.md`);
|
|
239
|
-
const { score, reviewVerdict } = await parseReview(reviewPath);
|
|
240
|
-
// Override status from review verdict if present
|
|
241
|
-
let status = specFields.status ?? "pending";
|
|
242
|
-
if (reviewVerdict === "APPROVED")
|
|
243
|
-
status = "approved";
|
|
244
|
-
else if (reviewVerdict === "NEEDS_WORK")
|
|
245
|
-
status = "needs-work";
|
|
246
|
-
else if (reviewVerdict === "REJECTED")
|
|
247
|
-
status = "rejected";
|
|
248
|
-
const commits = gitLog(stem);
|
|
249
|
-
tasks.push({
|
|
250
|
-
id: stem,
|
|
251
|
-
title: specFields.title ?? "",
|
|
252
|
-
feature: specFields.feature ?? "",
|
|
253
|
-
status,
|
|
254
|
-
score,
|
|
255
|
-
reviewVerdict,
|
|
256
|
-
commits,
|
|
257
|
-
specMtime: stat.mtime,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
// Sort newest first
|
|
261
|
-
tasks.sort((a, b) => b.specMtime.getTime() - a.specMtime.getTime());
|
|
262
|
-
if (tasks.length === 0) {
|
|
263
|
-
console.log(`No tasks found in the last ${days} days.`);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const fromDate = formatDateYMD(since);
|
|
267
|
-
const toDate = formatDateYMD(now);
|
|
268
|
-
const allCommits = tasks.flatMap(t => t.commits);
|
|
134
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
135
|
+
const commits = recentCommits(days);
|
|
269
136
|
const wikiEntries = await parseWikiLog(logPath, since);
|
|
270
|
-
if (
|
|
271
|
-
renderMarkdown(
|
|
137
|
+
if (format === "md") {
|
|
138
|
+
renderMarkdown(commits, wikiEntries, days);
|
|
272
139
|
}
|
|
273
140
|
else {
|
|
274
|
-
renderTable(
|
|
141
|
+
renderTable(commits, wikiEntries, days);
|
|
275
142
|
}
|
|
276
143
|
}
|