@doquflow/cli 1.5.1 → 1.6.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/init.js +125 -103
- package/dist/commands/recent.js +276 -0
- package/dist/commands/rewiki.js +339 -0
- package/dist/commands/ui.js +156 -1
- package/dist/index.js +22 -0
- package/package.json +2 -2
- package/ui-dist/assets/index-B20T-7YT.js +44 -0
- package/ui-dist/assets/index-DsJuaoK7.js +44 -0
- package/ui-dist/assets/index-lXOzEPMP.js +44 -0
- package/ui-dist/index.html +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerInGlobalRegistry = registerInGlobalRegistry;
|
|
7
7
|
exports.buildClaudeMd = buildClaudeMd;
|
|
8
8
|
exports.buildAgentsMd = buildAgentsMd;
|
|
9
|
+
exports.runInit = runInit;
|
|
9
10
|
exports.run = run;
|
|
10
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
12
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
@@ -153,11 +154,17 @@ lint_wiki({ project_path: "${projectDir}" })
|
|
|
153
154
|
→ fix orphans and broken refs
|
|
154
155
|
\`\`\`
|
|
155
156
|
|
|
157
|
+
### Maintenance — re-ingest with updated rules
|
|
158
|
+
\`\`\`
|
|
159
|
+
docuflow rewiki --dry-run # preview cleanup
|
|
160
|
+
docuflow rewiki # apply (backs up wiki first)
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
156
163
|
## Storage Layout
|
|
157
164
|
|
|
158
165
|
\`\`\`
|
|
159
166
|
.docuflow/
|
|
160
|
-
├── specs/
|
|
167
|
+
├── specs/ Spec files written by write_spec
|
|
161
168
|
├── wiki/ LLM-generated wiki pages
|
|
162
169
|
│ ├── entities/ Named things (services, APIs, databases)
|
|
163
170
|
│ ├── concepts/ Design patterns, principles, integrations
|
|
@@ -276,7 +283,16 @@ async function writeAgentsMd(projectDir) {
|
|
|
276
283
|
await promises_1.default.writeFile(agentsMdPath, newSection, "utf8");
|
|
277
284
|
}
|
|
278
285
|
}
|
|
279
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Core init logic — creates the .docuflow/ structure, registers MCP configs,
|
|
288
|
+
* writes CLAUDE.md/AGENTS.md, installs git hook, and registers in global registry.
|
|
289
|
+
*
|
|
290
|
+
* Accepts the target projectDir explicitly so it can be called from both the CLI
|
|
291
|
+
* (which uses process.cwd()) and the API (/api/init which receives the path from
|
|
292
|
+
* the request body).
|
|
293
|
+
*/
|
|
294
|
+
async function runInit(projectDir) {
|
|
295
|
+
const details = [];
|
|
280
296
|
const configPath = getClaudeDesktopConfigPath();
|
|
281
297
|
const vscodeConfigPath = getVSCodeMcpConfigPath();
|
|
282
298
|
const copilotCliConfigPath = getCopilotCliMcpConfigPath();
|
|
@@ -284,61 +300,59 @@ async function run() {
|
|
|
284
300
|
const serverBin = resolveServerBin();
|
|
285
301
|
const nodeBin = process.execPath;
|
|
286
302
|
// Register in Claude Desktop config
|
|
287
|
-
let config = {};
|
|
288
303
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
let config = {};
|
|
305
|
+
try {
|
|
306
|
+
const raw = await promises_1.default.readFile(configPath, "utf8");
|
|
307
|
+
config = JSON.parse(raw);
|
|
308
|
+
}
|
|
309
|
+
catch { /* File doesn't exist yet — that's fine */ }
|
|
310
|
+
if (!config.mcpServers)
|
|
311
|
+
config.mcpServers = {};
|
|
312
|
+
config.mcpServers.docuflow = { command: nodeBin, args: [serverBin] };
|
|
313
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
|
|
314
|
+
await promises_1.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
315
|
+
details.push("Claude Desktop MCP registered");
|
|
291
316
|
}
|
|
292
317
|
catch {
|
|
293
|
-
|
|
318
|
+
details.push("Claude Desktop — skipped (not installed)");
|
|
294
319
|
}
|
|
295
|
-
if (!config.mcpServers)
|
|
296
|
-
config.mcpServers = {};
|
|
297
|
-
config.mcpServers.docuflow = { command: nodeBin, args: [serverBin] };
|
|
298
|
-
await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
|
|
299
|
-
await promises_1.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
300
320
|
// Register in VS Code (GitHub Copilot) user MCP config
|
|
301
321
|
let vscodeRegistered = false;
|
|
302
|
-
let vscodeConfig = {};
|
|
303
|
-
try {
|
|
304
|
-
const raw = await promises_1.default.readFile(vscodeConfigPath, "utf8");
|
|
305
|
-
vscodeConfig = JSON.parse(raw);
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
// File may not exist — create it
|
|
309
|
-
}
|
|
310
|
-
if (!vscodeConfig.servers)
|
|
311
|
-
vscodeConfig.servers = {};
|
|
312
|
-
vscodeConfig.servers.docuflow = { command: nodeBin, args: [serverBin], type: "stdio" };
|
|
313
322
|
try {
|
|
323
|
+
let vscodeConfig = {};
|
|
324
|
+
try {
|
|
325
|
+
const raw = await promises_1.default.readFile(vscodeConfigPath, "utf8");
|
|
326
|
+
vscodeConfig = JSON.parse(raw);
|
|
327
|
+
}
|
|
328
|
+
catch { /* File may not exist */ }
|
|
329
|
+
if (!vscodeConfig.servers)
|
|
330
|
+
vscodeConfig.servers = {};
|
|
331
|
+
vscodeConfig.servers.docuflow = { command: nodeBin, args: [serverBin], type: "stdio" };
|
|
314
332
|
await promises_1.default.mkdir(node_path_1.default.dirname(vscodeConfigPath), { recursive: true });
|
|
315
333
|
await promises_1.default.writeFile(vscodeConfigPath, JSON.stringify(vscodeConfig, null, 2) + "\n", "utf8");
|
|
316
334
|
vscodeRegistered = true;
|
|
335
|
+
details.push("VS Code Copilot MCP registered");
|
|
317
336
|
}
|
|
318
|
-
catch {
|
|
319
|
-
// VS Code not installed or config dir not writable — skip silently
|
|
320
|
-
}
|
|
337
|
+
catch { /* VS Code not installed — skip */ }
|
|
321
338
|
// Register in GitHub Copilot CLI MCP config (~/.copilot/mcp-config.json)
|
|
322
339
|
let copilotCliRegistered = false;
|
|
323
|
-
let copilotCliConfig = {};
|
|
324
|
-
try {
|
|
325
|
-
const raw = await promises_1.default.readFile(copilotCliConfigPath, "utf8");
|
|
326
|
-
copilotCliConfig = JSON.parse(raw);
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
// File may not exist yet
|
|
330
|
-
}
|
|
331
|
-
if (!copilotCliConfig.mcpServers)
|
|
332
|
-
copilotCliConfig.mcpServers = {};
|
|
333
|
-
copilotCliConfig.mcpServers.docuflow = { type: "local", command: nodeBin, args: [serverBin], tools: ["*"] };
|
|
334
340
|
try {
|
|
341
|
+
let copilotCliConfig = {};
|
|
342
|
+
try {
|
|
343
|
+
const raw = await promises_1.default.readFile(copilotCliConfigPath, "utf8");
|
|
344
|
+
copilotCliConfig = JSON.parse(raw);
|
|
345
|
+
}
|
|
346
|
+
catch { /* File may not exist yet */ }
|
|
347
|
+
if (!copilotCliConfig.mcpServers)
|
|
348
|
+
copilotCliConfig.mcpServers = {};
|
|
349
|
+
copilotCliConfig.mcpServers.docuflow = { type: "local", command: nodeBin, args: [serverBin], tools: ["*"] };
|
|
335
350
|
await promises_1.default.mkdir(node_path_1.default.dirname(copilotCliConfigPath), { recursive: true });
|
|
336
351
|
await promises_1.default.writeFile(copilotCliConfigPath, JSON.stringify(copilotCliConfig, null, 2) + "\n", "utf8");
|
|
337
352
|
copilotCliRegistered = true;
|
|
353
|
+
details.push("Copilot CLI MCP registered");
|
|
338
354
|
}
|
|
339
|
-
catch {
|
|
340
|
-
// Copilot CLI not installed — skip silently
|
|
341
|
-
}
|
|
355
|
+
catch { /* Copilot CLI not installed — skip */ }
|
|
342
356
|
// Register in OpenAI Codex CLI (~/.codex/config.toml in TOML format)
|
|
343
357
|
let codexCliRegistered = false;
|
|
344
358
|
try {
|
|
@@ -353,99 +367,107 @@ async function run() {
|
|
|
353
367
|
await promises_1.default.writeFile(codexConfigPath, tomlContent + entry, "utf8");
|
|
354
368
|
}
|
|
355
369
|
codexCliRegistered = true;
|
|
370
|
+
details.push("Codex CLI MCP registered");
|
|
356
371
|
}
|
|
357
|
-
catch {
|
|
358
|
-
|
|
359
|
-
|
|
372
|
+
catch { /* Codex CLI not installed — skip */ }
|
|
373
|
+
// Suppress unused-variable warnings when variables are only used in console.log branches
|
|
374
|
+
void vscodeRegistered;
|
|
375
|
+
void copilotCliRegistered;
|
|
376
|
+
void codexCliRegistered;
|
|
360
377
|
// Create .docuflow/ directory structure
|
|
361
|
-
const projectDir = process.cwd();
|
|
362
378
|
const docuflowDir = node_path_1.default.join(projectDir, ".docuflow");
|
|
363
379
|
const specsDir = node_path_1.default.join(docuflowDir, "specs");
|
|
364
380
|
const wikiDir = node_path_1.default.join(docuflowDir, "wiki");
|
|
365
381
|
const sourcesDir = node_path_1.default.join(docuflowDir, "sources");
|
|
366
|
-
const entitiesDir = node_path_1.default.join(wikiDir, "entities");
|
|
367
|
-
const conceptsDir = node_path_1.default.join(wikiDir, "concepts");
|
|
368
|
-
const timelinesDir = node_path_1.default.join(wikiDir, "timelines");
|
|
369
|
-
const synthesesDir = node_path_1.default.join(wikiDir, "syntheses");
|
|
370
382
|
await promises_1.default.mkdir(specsDir, { recursive: true });
|
|
371
|
-
await promises_1.default.mkdir(
|
|
372
|
-
await promises_1.default.mkdir(
|
|
373
|
-
await promises_1.default.mkdir(
|
|
374
|
-
await promises_1.default.mkdir(
|
|
383
|
+
await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "entities"), { recursive: true });
|
|
384
|
+
await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "concepts"), { recursive: true });
|
|
385
|
+
await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "timelines"), { recursive: true });
|
|
386
|
+
await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "syntheses"), { recursive: true });
|
|
375
387
|
await promises_1.default.mkdir(sourcesDir, { recursive: true });
|
|
388
|
+
details.push("Created .docuflow/ directory structure");
|
|
376
389
|
// Copy or create template files
|
|
377
390
|
await copyTemplateFile("schema.md", node_path_1.default.join(docuflowDir, "schema.md"));
|
|
378
391
|
await copyTemplateFile("index.md", node_path_1.default.join(docuflowDir, "index.md"));
|
|
379
392
|
await copyTemplateFile("log.md", node_path_1.default.join(docuflowDir, "log.md"));
|
|
380
|
-
|
|
393
|
+
details.push("Wrote schema.md, index.md, log.md");
|
|
394
|
+
// Generate CLAUDE.md
|
|
381
395
|
await writeClaudeMd(projectDir);
|
|
382
|
-
|
|
396
|
+
details.push("Wrote CLAUDE.md");
|
|
397
|
+
// Generate AGENTS.md
|
|
383
398
|
await writeAgentsMd(projectDir);
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
const vscodeDirPath = node_path_1.default.join(projectDir, ".vscode");
|
|
387
|
-
const vscodeWorkspaceMcpPath = node_path_1.default.join(vscodeDirPath, "mcp.json");
|
|
388
|
-
let workspaceMcpConfig = {};
|
|
399
|
+
details.push("Wrote AGENTS.md");
|
|
400
|
+
// Write .vscode/mcp.json for project-level workspace MCP config
|
|
389
401
|
try {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
402
|
+
const vscodeDirPath = node_path_1.default.join(projectDir, ".vscode");
|
|
403
|
+
const vscodeWorkspaceMcpPath = node_path_1.default.join(vscodeDirPath, "mcp.json");
|
|
404
|
+
let workspaceMcpConfig = {};
|
|
405
|
+
try {
|
|
406
|
+
const raw = await promises_1.default.readFile(vscodeWorkspaceMcpPath, "utf8");
|
|
407
|
+
workspaceMcpConfig = JSON.parse(raw);
|
|
408
|
+
}
|
|
409
|
+
catch { /* File doesn't exist yet */ }
|
|
410
|
+
if (!workspaceMcpConfig.servers)
|
|
411
|
+
workspaceMcpConfig.servers = {};
|
|
412
|
+
workspaceMcpConfig.servers.docuflow = {
|
|
413
|
+
command: "npx",
|
|
414
|
+
args: ["-y", "-p", "@doquflow/server", "docuflow-server"],
|
|
415
|
+
type: "stdio",
|
|
416
|
+
};
|
|
417
|
+
await promises_1.default.mkdir(vscodeDirPath, { recursive: true });
|
|
418
|
+
await promises_1.default.writeFile(vscodeWorkspaceMcpPath, JSON.stringify(workspaceMcpConfig, null, 2) + "\n", "utf8");
|
|
419
|
+
details.push("Wrote .vscode/mcp.json");
|
|
395
420
|
}
|
|
396
|
-
|
|
397
|
-
workspaceMcpConfig.servers = {};
|
|
398
|
-
workspaceMcpConfig.servers.docuflow = {
|
|
399
|
-
command: "npx",
|
|
400
|
-
args: ["-y", "-p", "@doquflow/server", "docuflow-server"],
|
|
401
|
-
type: "stdio",
|
|
402
|
-
};
|
|
403
|
-
await promises_1.default.mkdir(vscodeDirPath, { recursive: true });
|
|
404
|
-
await promises_1.default.writeFile(vscodeWorkspaceMcpPath, JSON.stringify(workspaceMcpConfig, null, 2) + "\n", "utf8");
|
|
421
|
+
catch { /* non-fatal */ }
|
|
405
422
|
// Add .docuflow/ to .gitignore if present and not already listed
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
423
|
+
try {
|
|
424
|
+
const gitignorePath = node_path_1.default.join(projectDir, ".gitignore");
|
|
425
|
+
if (node_fs_1.default.existsSync(gitignorePath)) {
|
|
426
|
+
const gitignore = await promises_1.default.readFile(gitignorePath, "utf8");
|
|
427
|
+
if (!gitignore.includes(".docuflow/") && !gitignore.includes(".docuflow")) {
|
|
428
|
+
await promises_1.default.appendFile(gitignorePath, "\n# Docuflow\n.docuflow/\n");
|
|
429
|
+
details.push("Added .docuflow/ to .gitignore");
|
|
430
|
+
}
|
|
411
431
|
}
|
|
412
432
|
}
|
|
413
|
-
|
|
433
|
+
catch { /* non-fatal */ }
|
|
434
|
+
// Install git post-commit hook
|
|
414
435
|
await installGitHook(projectDir);
|
|
415
|
-
|
|
436
|
+
details.push("Installed git post-commit hook");
|
|
437
|
+
// Register in global project registry
|
|
416
438
|
await registerInGlobalRegistry(projectDir);
|
|
417
|
-
|
|
439
|
+
details.push("Registered in global project registry (~/.docuflow/projects.json)");
|
|
440
|
+
return { ok: true, path: projectDir, details };
|
|
441
|
+
}
|
|
442
|
+
async function run() {
|
|
443
|
+
const result = await runInit(process.cwd());
|
|
444
|
+
const docuflowDir = node_path_1.default.join(result.path, ".docuflow");
|
|
445
|
+
console.log("✓ DocuFlow initialised successfully.");
|
|
418
446
|
console.log("");
|
|
419
|
-
console.log("
|
|
447
|
+
console.log("📁 Structure created:");
|
|
420
448
|
console.log(` ${docuflowDir}/`);
|
|
421
|
-
console.log(`
|
|
422
|
-
console.log(`
|
|
423
|
-
console.log(`
|
|
424
|
-
console.log(`
|
|
425
|
-
console.log(`
|
|
426
|
-
console.log(`
|
|
427
|
-
console.log(`
|
|
428
|
-
console.log(`
|
|
429
|
-
console.log(`
|
|
430
|
-
console.log(`
|
|
449
|
+
console.log(` ├── specs/ (code specs written by the agent)`);
|
|
450
|
+
console.log(` ├── wiki/ (LLM-generated wiki pages)`);
|
|
451
|
+
console.log(` │ ├── entities/`);
|
|
452
|
+
console.log(` │ ├── concepts/`);
|
|
453
|
+
console.log(` │ ├── timelines/`);
|
|
454
|
+
console.log(` │ └── syntheses/`);
|
|
455
|
+
console.log(` ├── sources/ (raw markdown documents to ingest)`);
|
|
456
|
+
console.log(` ├── schema.md (wiki configuration)`);
|
|
457
|
+
console.log(` ├── index.md (auto-maintained catalog)`);
|
|
458
|
+
console.log(` └── log.md (operation log)`);
|
|
431
459
|
console.log("");
|
|
432
|
-
console.log("
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
console.log("\ud83d\udd27 MCP Registration:");
|
|
437
|
-
console.log(` Claude Desktop: \u2713 registered`);
|
|
438
|
-
console.log(` VS Code Copilot: ${vscodeRegistered ? "\u2713 registered (user-level)" : "\u2014 not detected"}`);
|
|
439
|
-
console.log(` Copilot CLI: ${copilotCliRegistered ? "\u2713 registered (~/.copilot/mcp-config.json)" : "\u2014 not detected"}`);
|
|
440
|
-
console.log(` Codex CLI: ${codexCliRegistered ? "\u2713 registered (~/.codex/config.toml)" : "\u2014 not detected"}`);
|
|
441
|
-
console.log(` Workspace: \u2713 .vscode/mcp.json written (commit to share with team)`);
|
|
460
|
+
console.log("📝 Steps completed:");
|
|
461
|
+
for (const line of result.details) {
|
|
462
|
+
console.log(` ✓ ${line}`);
|
|
463
|
+
}
|
|
442
464
|
console.log("");
|
|
443
|
-
console.log("
|
|
465
|
+
console.log("📖 Next steps:");
|
|
444
466
|
console.log(" 1. Edit .docuflow/schema.md to customize your wiki domain");
|
|
445
467
|
console.log(" 2. Add markdown docs to .docuflow/sources/ then ingest them");
|
|
446
468
|
console.log(" 3. Restart Claude Desktop / reload VS Code / restart Copilot CLI");
|
|
447
469
|
console.log("");
|
|
448
|
-
console.log("
|
|
470
|
+
console.log("⚡ Auto-sync options:");
|
|
449
471
|
console.log(" docuflow watch # background daemon (watches for file changes)");
|
|
450
472
|
console.log(" docuflow watch --ai # + Claude/Codex documents code changes automatically");
|
|
451
473
|
console.log(" docuflow sync # one-shot sync (good for CI/CD)");
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docuflow recent
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.run = run;
|
|
14
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
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
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
23
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
24
|
+
};
|
|
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
|
+
function formatDateYMD(d) {
|
|
51
|
+
return d.toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
// ── Git helper ───────────────────────────────────────────────────────────────
|
|
54
|
+
function gitLog(taskId) {
|
|
55
|
+
try {
|
|
56
|
+
const out = (0, node_child_process_1.execSync)(`git log --oneline --all --grep="${taskId}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
57
|
+
return out.trim().split("\n").filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
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
|
|
117
|
+
async function parseWikiLog(logPath, since) {
|
|
118
|
+
let content;
|
|
119
|
+
try {
|
|
120
|
+
content = await promises_1.default.readFile(logPath, "utf8");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const entries = [];
|
|
126
|
+
const headingRe = /^##\s+\[([^\]]+)\]\s+([^|]+)\|\s*(.+)$/;
|
|
127
|
+
for (const line of content.split("\n")) {
|
|
128
|
+
const m = line.match(headingRe);
|
|
129
|
+
if (!m)
|
|
130
|
+
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
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
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));
|
|
154
|
+
}
|
|
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}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (wikiEntries.length > 0) {
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(c.bold(`Wiki Activity (last ${days} days)`));
|
|
166
|
+
for (const e of wikiEntries) {
|
|
167
|
+
console.log(` ${e.date} ${e.operation} ${e.file}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate) {
|
|
172
|
+
console.log(`# Recent Work — last ${days} days (${fromDate} → ${toDate})`);
|
|
173
|
+
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} |`);
|
|
180
|
+
}
|
|
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) {
|
|
186
|
+
console.log(`- ${line}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (wikiEntries.length > 0) {
|
|
190
|
+
console.log("");
|
|
191
|
+
console.log(`## Wiki Activity (last ${days} days)`);
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log("| Date | Operation | File |");
|
|
194
|
+
console.log("|------|-----------|------|");
|
|
195
|
+
for (const e of wikiEntries) {
|
|
196
|
+
console.log(`| ${e.date} | ${e.operation} | ${e.file} |`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Main entry point ─────────────────────────────────────────────────────────
|
|
201
|
+
async function run(opts = { days: 7, format: "table" }) {
|
|
202
|
+
const days = Math.max(1, isNaN(opts.days) ? 7 : opts.days);
|
|
203
|
+
const format = opts.format ?? "table";
|
|
204
|
+
const isMd = format === "md";
|
|
205
|
+
const cwd = process.cwd();
|
|
206
|
+
const specsDir = node_path_1.default.join(cwd, ".devloop", "specs");
|
|
207
|
+
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);
|
|
269
|
+
const wikiEntries = await parseWikiLog(logPath, since);
|
|
270
|
+
if (isMd) {
|
|
271
|
+
renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
renderTable(tasks, allCommits, wikiEntries, days, fromDate, toDate);
|
|
275
|
+
}
|
|
276
|
+
}
|