@doquflow/cli 1.6.0 → 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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * docuflow recent
4
4
  *
5
- * Aggregates recent work by scanning .devloop/specs/TASK-*.md files,
6
- * correlating git commits by task ID, reading .docuflow/log.md for wiki
7
- * activity, and rendering a formatted dashboard in the terminal.
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 helper ───────────────────────────────────────────────────────────────
54
- function gitLog(taskId) {
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 --all --grep="${taskId}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
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
- // ── Spec parser ───────────────────────────────────────────────────────────────
64
- async function parseSpec(specPath) {
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.match(headingRe);
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(tasks, allCommits, wikiEntries, days, fromDate, toDate) {
143
- const SEP = "─".repeat(72);
144
- console.log(c.bold(`Recent Work last ${days} days (${fromDate} → ${toDate})`));
145
- console.log(SEP);
146
- console.log(` ${"TASK".padEnd(24)} ${"TITLE".padEnd(40)} ${"STATUS".padEnd(12)} SCORE`);
147
- console.log(SEP);
148
- for (const t of tasks) {
149
- const title = truncate(t.title || t.feature, 40).padEnd(40);
150
- const status = t.status.padEnd(12);
151
- const score = t.score ?? "—";
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
- console.log(SEP);
156
- if (allCommits.length > 0) {
157
- console.log("");
158
- console.log(c.bold(`Git Commits (${allCommits.length} matching)`));
159
- for (const line of allCommits) {
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
- if (wikiEntries.length > 0) {
164
- console.log("");
165
- console.log(c.bold(`Wiki Activity (last ${days} days)`));
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} ${e.file}`);
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(tasks, allCommits, wikiEntries, days, fromDate, toDate) {
172
- console.log(`# Recent Work last ${days} days (${fromDate} → ${toDate})`);
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
- console.log("| TASK | TITLE | STATUS | SCORE |");
175
- console.log("|------|-------|--------|-------|");
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
- if (allCommits.length > 0) {
182
- console.log("");
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
- if (wikiEntries.length > 0) {
190
- console.log("");
191
- console.log(`## Wiki Activity (last ${days} days)`);
192
- console.log("");
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 now = new Date();
209
- const since = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
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 (isMd) {
271
- renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate);
137
+ if (format === "md") {
138
+ renderMarkdown(commits, wikiEntries, days);
272
139
  }
273
140
  else {
274
- renderTable(tasks, allCommits, wikiEntries, days, fromDate, toDate);
141
+ renderTable(commits, wikiEntries, days);
275
142
  }
276
143
  }