@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
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docuflow rewiki
|
|
4
|
+
*
|
|
5
|
+
* Re-ingests all sources with the current extractor rules, migrates any
|
|
6
|
+
* synthesiss/ typo (defense-in-depth), backs up the existing wiki, and
|
|
7
|
+
* produces an audit report of removed / preserved pages.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* docuflow rewiki # full migration with backup
|
|
11
|
+
* docuflow rewiki --dry-run # preview what would change, no writes
|
|
12
|
+
* docuflow rewiki --no-backup # skip backup (faster, irreversible)
|
|
13
|
+
* docuflow rewiki --quiet # suppress progress output
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 — success
|
|
17
|
+
* 2 — fatal error (.docuflow missing, server tools not found)
|
|
18
|
+
*/
|
|
19
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.run = run;
|
|
24
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
// ─── Colour helpers ────────────────────────────────────────────────────────────
|
|
28
|
+
const c = {
|
|
29
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
30
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
31
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
32
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
33
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
34
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
35
|
+
};
|
|
36
|
+
// ─── Dynamic server tool loader (mirrors sync.ts pattern) ─────────────────────
|
|
37
|
+
function loadServerTool(toolFile) {
|
|
38
|
+
const candidates = [
|
|
39
|
+
() => require(`@doquflow/server/dist/tools/${toolFile}`),
|
|
40
|
+
() => require(node_path_1.default.resolve(__dirname, "../../../server/dist/tools", toolFile)),
|
|
41
|
+
() => require(node_path_1.default.resolve(__dirname, "../../server/dist/tools", toolFile)),
|
|
42
|
+
];
|
|
43
|
+
for (const attempt of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
return attempt();
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Cannot load server tool "${toolFile}". Run "npm run build" first.`);
|
|
50
|
+
}
|
|
51
|
+
function parseTags(content) {
|
|
52
|
+
const m = content.match(/^tags:\s*(\[.*?\])/m);
|
|
53
|
+
if (!m)
|
|
54
|
+
return [];
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(m[1]);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function scanWiki(wikiDir) {
|
|
63
|
+
const pages = [];
|
|
64
|
+
if (!node_fs_1.default.existsSync(wikiDir))
|
|
65
|
+
return pages;
|
|
66
|
+
const subdirs = await promises_1.default.readdir(wikiDir).catch(() => []);
|
|
67
|
+
for (const subdir of subdirs) {
|
|
68
|
+
const subdirPath = node_path_1.default.join(wikiDir, subdir);
|
|
69
|
+
const stat = await promises_1.default.stat(subdirPath).catch(() => null);
|
|
70
|
+
if (!stat?.isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
const files = await promises_1.default.readdir(subdirPath).catch(() => []);
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (!file.endsWith(".md"))
|
|
75
|
+
continue;
|
|
76
|
+
const filePath = node_path_1.default.join(subdirPath, file);
|
|
77
|
+
const content = await promises_1.default.readFile(filePath, "utf8").catch(() => "");
|
|
78
|
+
const tags = parseTags(content);
|
|
79
|
+
pages.push({
|
|
80
|
+
filePath,
|
|
81
|
+
pageId: file.replace(/\.md$/, ""),
|
|
82
|
+
category: subdir,
|
|
83
|
+
tags,
|
|
84
|
+
isUserSaved: tags.includes("query_result"),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return pages;
|
|
89
|
+
}
|
|
90
|
+
// ─── Recursive copy ────────────────────────────────────────────────────────────
|
|
91
|
+
async function copyDir(src, dest) {
|
|
92
|
+
await promises_1.default.mkdir(dest, { recursive: true });
|
|
93
|
+
const entries = await promises_1.default.readdir(src, { withFileTypes: true });
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const srcPath = node_path_1.default.join(src, entry.name);
|
|
96
|
+
const destPath = node_path_1.default.join(dest, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
await copyDir(srcPath, destPath);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
await promises_1.default.copyFile(srcPath, destPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ─── Count pages by category ───────────────────────────────────────────────────
|
|
106
|
+
function countByCategory(pages) {
|
|
107
|
+
const counts = {};
|
|
108
|
+
for (const p of pages) {
|
|
109
|
+
counts[p.category] = (counts[p.category] ?? 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
return counts;
|
|
112
|
+
}
|
|
113
|
+
async function run(options = {}) {
|
|
114
|
+
const projectPath = node_path_1.default.resolve(process.cwd());
|
|
115
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
116
|
+
const sourcesDir = node_path_1.default.join(docuDir, "sources");
|
|
117
|
+
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
118
|
+
const dryRun = options.dryRun ?? false;
|
|
119
|
+
const quiet = options.quiet ?? false;
|
|
120
|
+
function info(msg) { if (!quiet)
|
|
121
|
+
console.log(msg); }
|
|
122
|
+
// ── Step 1: Pre-flight ──────────────────────────────────────────────────────
|
|
123
|
+
info(c.bold(`\n 🔄 DocuFlow Rewiki${dryRun ? c.yellow(" (dry-run)") : ""}\n`));
|
|
124
|
+
if (!node_fs_1.default.existsSync(docuDir)) {
|
|
125
|
+
console.error(c.red(` ✗ .docuflow/ not found at ${projectPath}`));
|
|
126
|
+
console.error(` Run "docuflow init" first.`);
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
|
129
|
+
if (!node_fs_1.default.existsSync(sourcesDir)) {
|
|
130
|
+
console.error(c.red(` ✗ .docuflow/sources/ not found`));
|
|
131
|
+
console.error(` Nothing to re-ingest.`);
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
// Count sources
|
|
135
|
+
const sourceFiles = (await promises_1.default.readdir(sourcesDir).catch(() => []))
|
|
136
|
+
.filter(f => f.endsWith(".md"));
|
|
137
|
+
info(` 📚 Sources: ${sourceFiles.length} file(s)`);
|
|
138
|
+
// Scan current wiki
|
|
139
|
+
const pagesBefore = await scanWiki(wikiDir);
|
|
140
|
+
const countsBefore = countByCategory(pagesBefore);
|
|
141
|
+
info(` 📖 Wiki pages before:`);
|
|
142
|
+
for (const [cat, n] of Object.entries(countsBefore)) {
|
|
143
|
+
info(` ${cat}: ${n}`);
|
|
144
|
+
}
|
|
145
|
+
if (pagesBefore.length === 0) {
|
|
146
|
+
info(c.yellow(` (none)`));
|
|
147
|
+
}
|
|
148
|
+
const userSavedPages = pagesBefore.filter(p => p.isUserSaved);
|
|
149
|
+
info(` 👤 User-saved synthesis pages: ${userSavedPages.length} (will be preserved)`);
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
info(c.yellow(`\n ℹ️ Dry-run mode — no files will be written.\n`));
|
|
152
|
+
}
|
|
153
|
+
// ── Step 2: Backup ──────────────────────────────────────────────────────────
|
|
154
|
+
let backupPath = "skipped";
|
|
155
|
+
if (!options.noBackup && !dryRun) {
|
|
156
|
+
const isoStamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
157
|
+
backupPath = node_path_1.default.join(docuDir, `wiki.backup-${isoStamp}`);
|
|
158
|
+
info(`\n 💾 Backing up wiki → ${c.cyan(node_path_1.default.relative(projectPath, backupPath))}`);
|
|
159
|
+
if (node_fs_1.default.existsSync(wikiDir)) {
|
|
160
|
+
await copyDir(wikiDir, backupPath);
|
|
161
|
+
info(c.green(` ✅ Backup complete`));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
info(c.yellow(` ⚠ wiki/ does not exist — skipping backup`));
|
|
165
|
+
backupPath = "skipped (wiki/ missing)";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (options.noBackup) {
|
|
169
|
+
info(`\n ⚠ Backup skipped (--no-backup)`);
|
|
170
|
+
}
|
|
171
|
+
// ── Step 3: Migrate synthesiss/ typo (defense-in-depth) ────────────────────
|
|
172
|
+
const synthesissDir = node_path_1.default.join(wikiDir, "synthesiss");
|
|
173
|
+
const synthesesDir = node_path_1.default.join(wikiDir, "syntheses");
|
|
174
|
+
const migratedFiles = [];
|
|
175
|
+
if (!dryRun && node_fs_1.default.existsSync(synthesissDir)) {
|
|
176
|
+
info(`\n 🔧 Migrating synthesiss/ → syntheses/ (typo fix)`);
|
|
177
|
+
await promises_1.default.mkdir(synthesesDir, { recursive: true });
|
|
178
|
+
const files = await promises_1.default.readdir(synthesissDir).catch(() => []);
|
|
179
|
+
for (const f of files) {
|
|
180
|
+
const src = node_path_1.default.join(synthesissDir, f);
|
|
181
|
+
const dest = node_path_1.default.join(synthesesDir, f);
|
|
182
|
+
await promises_1.default.rename(src, dest);
|
|
183
|
+
migratedFiles.push(f);
|
|
184
|
+
info(c.dim(` → ${f}`));
|
|
185
|
+
}
|
|
186
|
+
await promises_1.default.rmdir(synthesissDir).catch(() => { });
|
|
187
|
+
info(c.green(` ✅ Migrated ${migratedFiles.length} file(s), removed synthesiss/`));
|
|
188
|
+
}
|
|
189
|
+
else if (dryRun && node_fs_1.default.existsSync(synthesissDir)) {
|
|
190
|
+
const files = await promises_1.default.readdir(synthesissDir).catch(() => []);
|
|
191
|
+
info(`\n 🔧 [dry-run] Would migrate ${files.length} file(s) from synthesiss/ → syntheses/`);
|
|
192
|
+
migratedFiles.push(...files);
|
|
193
|
+
}
|
|
194
|
+
// ── Step 4: Clear auto-generated pages & re-ingest ─────────────────────────
|
|
195
|
+
const pagesAfterIds = new Set();
|
|
196
|
+
let ingestErrors = 0;
|
|
197
|
+
const rejectedPages = [];
|
|
198
|
+
if (!dryRun) {
|
|
199
|
+
// Delete auto-generated pages (not user-saved) before re-ingesting
|
|
200
|
+
// so old noise pages are removed even if no new equivalent is created
|
|
201
|
+
info(`\n 🗑 Clearing auto-generated wiki pages...`);
|
|
202
|
+
let cleared = 0;
|
|
203
|
+
for (const page of pagesBefore) {
|
|
204
|
+
if (!page.isUserSaved) {
|
|
205
|
+
await promises_1.default.unlink(page.filePath).catch(() => { });
|
|
206
|
+
cleared++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
info(` Cleared ${cleared} auto-generated page(s) (${userSavedPages.length} user-saved preserved)`);
|
|
210
|
+
// Load server tools
|
|
211
|
+
let ingestSource;
|
|
212
|
+
let updateIndex;
|
|
213
|
+
try {
|
|
214
|
+
({ ingestSource } = loadServerTool("ingest-source"));
|
|
215
|
+
({ updateIndex } = loadServerTool("update-index"));
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
console.error(c.red(` ✗ ${e.message}`));
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
// Re-ingest all sources
|
|
222
|
+
info(`\n 📥 Re-ingesting ${sourceFiles.length} source file(s)...`);
|
|
223
|
+
for (const filename of sourceFiles) {
|
|
224
|
+
try {
|
|
225
|
+
const result = await ingestSource({ project_path: projectPath, source_filename: filename });
|
|
226
|
+
const created = result.pages_created ?? [];
|
|
227
|
+
for (const id of created)
|
|
228
|
+
pagesAfterIds.add(id);
|
|
229
|
+
info(` ${c.green("✓")} ${filename} → ${created.length} page(s)`);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
info(c.red(` ✗ ${filename}: ${e.message}`));
|
|
233
|
+
ingestErrors++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ── Step 5: Update index ────────────────────────────────────────────────
|
|
237
|
+
info(`\n 📋 Rebuilding index...`);
|
|
238
|
+
const indexResult = await updateIndex({ project_path: projectPath });
|
|
239
|
+
info(c.green(` ✅ Index rebuilt — ${indexResult.entries_indexed ?? "?"} entries`));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Dry-run: simulate what would happen
|
|
243
|
+
info(`\n 📥 [dry-run] Would re-ingest ${sourceFiles.length} source file(s)`);
|
|
244
|
+
let { ingestSource } = (() => {
|
|
245
|
+
try {
|
|
246
|
+
return loadServerTool("ingest-source");
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return { ingestSource: null };
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
if (ingestSource) {
|
|
253
|
+
for (const filename of sourceFiles) {
|
|
254
|
+
try {
|
|
255
|
+
const result = await ingestSource({ project_path: projectPath, source_filename: filename });
|
|
256
|
+
for (const id of (result.pages_created ?? []))
|
|
257
|
+
pagesAfterIds.add(id);
|
|
258
|
+
info(` ${c.dim("~")} ${filename} → ${(result.pages_created ?? []).length} page(s) (dry-run)`);
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// ── Pages removed = before - after - user-saved ───────────────────────────
|
|
265
|
+
for (const page of pagesBefore) {
|
|
266
|
+
if (!page.isUserSaved && !pagesAfterIds.has(page.pageId)) {
|
|
267
|
+
rejectedPages.push({ pageId: page.pageId, category: page.category });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ── Step 6: Write report ────────────────────────────────────────────────────
|
|
271
|
+
const reportPath = node_path_1.default.join(docuDir, "rewiki-report.md");
|
|
272
|
+
const pagesAfter = dryRun ? pagesBefore : await scanWiki(wikiDir);
|
|
273
|
+
const countsAfter = countByCategory(pagesAfter);
|
|
274
|
+
const now = new Date().toISOString();
|
|
275
|
+
const reportLines = [
|
|
276
|
+
`# DocuFlow Rewiki Report`,
|
|
277
|
+
``,
|
|
278
|
+
`**Timestamp:** ${now}`,
|
|
279
|
+
`**Dry-run:** ${dryRun}`,
|
|
280
|
+
`**Backup path:** ${backupPath}`,
|
|
281
|
+
``,
|
|
282
|
+
`## Pages Before`,
|
|
283
|
+
``,
|
|
284
|
+
...Object.entries(countsBefore).map(([cat, n]) => `- ${cat}: ${n}`),
|
|
285
|
+
`- **Total:** ${pagesBefore.length}`,
|
|
286
|
+
``,
|
|
287
|
+
`## Pages After`,
|
|
288
|
+
``,
|
|
289
|
+
...Object.entries(countsAfter).map(([cat, n]) => `- ${cat}: ${n}`),
|
|
290
|
+
`- **Total:** ${pagesAfter.length}`,
|
|
291
|
+
``,
|
|
292
|
+
`## Removed Pages (${rejectedPages.length})`,
|
|
293
|
+
``,
|
|
294
|
+
rejectedPages.length > 0
|
|
295
|
+
? rejectedPages.map(p => `- \`${p.pageId}\` (${p.category})`).join("\n")
|
|
296
|
+
: "None.",
|
|
297
|
+
``,
|
|
298
|
+
`## Preserved User-Saved Pages (${userSavedPages.length})`,
|
|
299
|
+
``,
|
|
300
|
+
userSavedPages.length > 0
|
|
301
|
+
? userSavedPages.map(p => `- \`${p.pageId}\``).join("\n")
|
|
302
|
+
: "None.",
|
|
303
|
+
``,
|
|
304
|
+
];
|
|
305
|
+
if (migratedFiles.length > 0) {
|
|
306
|
+
reportLines.push(`## synthesiss/ Migrations (${migratedFiles.length})`, ``, ...migratedFiles.map(f => `- ${f}`), ``);
|
|
307
|
+
}
|
|
308
|
+
const reportContent = reportLines.join("\n");
|
|
309
|
+
if (!dryRun) {
|
|
310
|
+
await promises_1.default.writeFile(reportPath, reportContent, "utf8");
|
|
311
|
+
info(`\n 📄 Report written → ${c.cyan(node_path_1.default.relative(projectPath, reportPath))}`);
|
|
312
|
+
// ── Step 7: Append to log.md ──────────────────────────────────────────────
|
|
313
|
+
const logPath = node_path_1.default.join(docuDir, "log.md");
|
|
314
|
+
const logEntry = `\n## [${now.split("T")[0]}] rewiki | removed ${rejectedPages.length}, kept ${pagesAfter.length - userSavedPages.length}, preserved ${userSavedPages.length} user pages\n`;
|
|
315
|
+
try {
|
|
316
|
+
await promises_1.default.appendFile(logPath, logEntry, "utf8");
|
|
317
|
+
}
|
|
318
|
+
catch { }
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
info(`\n 📄 [dry-run] Report would be written to ${c.cyan(node_path_1.default.relative(projectPath, reportPath))}`);
|
|
322
|
+
}
|
|
323
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
324
|
+
info(`\n ─────────────────────────────────────────────`);
|
|
325
|
+
info(` Sources re-ingested: ${dryRun ? "(dry-run)" : sourceFiles.length}`);
|
|
326
|
+
info(` Pages before: ${pagesBefore.length}`);
|
|
327
|
+
info(` Pages after: ${dryRun ? "(dry-run)" : pagesAfter.length}`);
|
|
328
|
+
info(` Removed: ${rejectedPages.length}`);
|
|
329
|
+
info(` Preserved (user): ${userSavedPages.length}`);
|
|
330
|
+
if (migratedFiles.length > 0) {
|
|
331
|
+
info(` synthesiss/ migrated: ${migratedFiles.length}`);
|
|
332
|
+
}
|
|
333
|
+
if (ingestErrors > 0) {
|
|
334
|
+
info(` Ingest errors: ${c.red(String(ingestErrors))}`);
|
|
335
|
+
}
|
|
336
|
+
info(` ─────────────────────────────────────────────\n`);
|
|
337
|
+
if (ingestErrors > 0)
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
package/dist/commands/ui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* docuflow ui / docuflow start
|
|
4
4
|
*
|
|
5
5
|
* Starts the DocuFlow web interface — a single Express server on port 48821 that serves:
|
|
6
|
-
* • All /api/* routes (projects, wiki, health, activity, ask, search)
|
|
6
|
+
* • All /api/* routes (projects, wiki, health, activity, ask, search, sync, init, watch)
|
|
7
7
|
* • Static Vite build from ui-dist/ (bundled with this CLI package)
|
|
8
8
|
* • SPA fallback — any non-API, non-asset route returns index.html
|
|
9
9
|
*
|
|
@@ -23,6 +23,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
23
23
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
24
24
|
const express_1 = __importDefault(require("express"));
|
|
25
25
|
const cors_1 = __importDefault(require("cors"));
|
|
26
|
+
const init_1 = require("./init");
|
|
27
|
+
const watch_1 = require("./watch");
|
|
26
28
|
function loadTool(file, exportName) {
|
|
27
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
30
|
return require(`@doquflow/server/dist/tools/${file}`)[exportName];
|
|
@@ -203,6 +205,8 @@ async function run(opts = {}) {
|
|
|
203
205
|
const queryWikiTool = loadTool('query-wiki', 'queryWiki');
|
|
204
206
|
const wikiSearchTool = loadTool('wiki-search', 'wikiSearch');
|
|
205
207
|
const buildGraphTool = loadTool('build-graph', 'buildGraph');
|
|
208
|
+
const ingestSourceTool = loadTool('ingest-source', 'ingestSource');
|
|
209
|
+
const updateIndexTool = loadTool('update-index', 'updateIndex');
|
|
206
210
|
// ── 3. Express app ──────────────────────────────────────────────────────
|
|
207
211
|
const app = (0, express_1.default)();
|
|
208
212
|
app.use((0, cors_1.default)());
|
|
@@ -359,6 +363,157 @@ async function run(opts = {}) {
|
|
|
359
363
|
return res.status(500).json({ error: e.message });
|
|
360
364
|
}
|
|
361
365
|
});
|
|
366
|
+
// ── Sync: ingest all sources → rebuild index → lint ──────────────────────
|
|
367
|
+
app.post('/api/sync', async (req, res) => {
|
|
368
|
+
const { path: projectPath } = req.body;
|
|
369
|
+
if (!projectPath)
|
|
370
|
+
return res.status(400).json({ error: 'path required' });
|
|
371
|
+
const docuDir = node_path_1.default.join(projectPath, '.docuflow');
|
|
372
|
+
const sourcesDir = node_path_1.default.join(docuDir, 'sources');
|
|
373
|
+
if (!node_fs_1.default.existsSync(docuDir)) {
|
|
374
|
+
return res.status(400).json({ error: '.docuflow not found — run init first' });
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
// Collect source files
|
|
378
|
+
let sourceFiles = [];
|
|
379
|
+
try {
|
|
380
|
+
sourceFiles = (await promises_1.default.readdir(sourcesDir)).filter(f => f.endsWith('.md'));
|
|
381
|
+
}
|
|
382
|
+
catch { /* sources/ may not exist yet */ }
|
|
383
|
+
// Ingest each source file
|
|
384
|
+
let pagesCreated = 0;
|
|
385
|
+
const errors = [];
|
|
386
|
+
for (const filename of sourceFiles) {
|
|
387
|
+
try {
|
|
388
|
+
const r = await ingestSourceTool({ project_path: projectPath, source_filename: filename });
|
|
389
|
+
pagesCreated += r.pages_created?.length ?? 0;
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
errors.push(`${filename}: ${e.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Rebuild index and lint — wiki dir may be empty on a fresh/blank project
|
|
396
|
+
let health_score = 0;
|
|
397
|
+
try {
|
|
398
|
+
await updateIndexTool({ project_path: projectPath });
|
|
399
|
+
const lint = await lintWikiTool({ project_path: projectPath });
|
|
400
|
+
health_score = lint.health_score ?? 0;
|
|
401
|
+
}
|
|
402
|
+
catch { /* empty wiki dir — health_score stays 0 */ }
|
|
403
|
+
return res.json({
|
|
404
|
+
sources_processed: sourceFiles.length,
|
|
405
|
+
pages_created: pagesCreated,
|
|
406
|
+
health_score,
|
|
407
|
+
errors,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
return res.status(500).json({ error: e.message });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
// ── Init: create .docuflow/ structure and register project ────────────────
|
|
415
|
+
app.post('/api/init', async (req, res) => {
|
|
416
|
+
const { path: projectPath } = req.body;
|
|
417
|
+
if (!projectPath)
|
|
418
|
+
return res.status(400).json({ error: 'path required' });
|
|
419
|
+
if (!node_fs_1.default.existsSync(projectPath)) {
|
|
420
|
+
return res.status(400).json({ error: 'path does not exist' });
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const result = await (0, init_1.runInit)(projectPath);
|
|
424
|
+
return res.json(result);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
return res.status(500).json({ error: e.message });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
// ── Watch daemon status / stop / start ────────────────────────────────────
|
|
431
|
+
app.get('/api/watch/status', async (req, res) => {
|
|
432
|
+
const projectPath = req.query.path;
|
|
433
|
+
if (!projectPath)
|
|
434
|
+
return res.status(400).json({ error: 'path required' });
|
|
435
|
+
try {
|
|
436
|
+
const data = await (0, watch_1.readPidFile)(projectPath);
|
|
437
|
+
if (!data)
|
|
438
|
+
return res.json({ running: false });
|
|
439
|
+
const alive = (0, watch_1.isProcessAlive)(data.pid);
|
|
440
|
+
if (!alive)
|
|
441
|
+
return res.json({ running: false });
|
|
442
|
+
const uptimeMs = Date.now() - new Date(data.started_at).getTime();
|
|
443
|
+
const uptimeMin = Math.floor(uptimeMs / 60_000);
|
|
444
|
+
return res.json({
|
|
445
|
+
running: true,
|
|
446
|
+
pid: data.pid,
|
|
447
|
+
bridge: data.bridge,
|
|
448
|
+
started_at: data.started_at,
|
|
449
|
+
uptime: uptimeMin < 60 ? `${uptimeMin}m` : `${Math.floor(uptimeMin / 60)}h ${uptimeMin % 60}m`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
return res.status(500).json({ error: e.message });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
app.post('/api/watch/stop', async (req, res) => {
|
|
457
|
+
const { path: projectPath } = req.body;
|
|
458
|
+
if (!projectPath)
|
|
459
|
+
return res.status(400).json({ error: 'path required' });
|
|
460
|
+
try {
|
|
461
|
+
const data = await (0, watch_1.readPidFile)(projectPath);
|
|
462
|
+
if (!data || !(0, watch_1.isProcessAlive)(data.pid)) {
|
|
463
|
+
return res.json({ ok: true, message: 'Daemon not running' });
|
|
464
|
+
}
|
|
465
|
+
process.kill(data.pid, 'SIGTERM');
|
|
466
|
+
// Remove stale PID file — daemon will also try to clean it up on exit
|
|
467
|
+
setTimeout(async () => {
|
|
468
|
+
try {
|
|
469
|
+
await promises_1.default.unlink((0, watch_1.getPidFilePath)(projectPath));
|
|
470
|
+
}
|
|
471
|
+
catch { /* already removed */ }
|
|
472
|
+
}, 2000);
|
|
473
|
+
return res.json({ ok: true, message: `Stopped watch daemon (PID ${data.pid})` });
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
return res.status(500).json({ error: e.message });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
app.post('/api/watch/start', async (req, res) => {
|
|
480
|
+
const { path: projectPath } = req.body;
|
|
481
|
+
if (!projectPath)
|
|
482
|
+
return res.status(400).json({ error: 'path required' });
|
|
483
|
+
if (!node_fs_1.default.existsSync(projectPath)) {
|
|
484
|
+
return res.status(400).json({ error: 'path does not exist' });
|
|
485
|
+
}
|
|
486
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(projectPath, '.docuflow'))) {
|
|
487
|
+
return res.status(400).json({ error: '.docuflow not found — run init first' });
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
// Check if already running
|
|
491
|
+
const existing = await (0, watch_1.readPidFile)(projectPath);
|
|
492
|
+
if (existing && (0, watch_1.isProcessAlive)(existing.pid)) {
|
|
493
|
+
return res.json({ ok: true, message: 'Watch daemon already running', pid: existing.pid });
|
|
494
|
+
}
|
|
495
|
+
// require.resolve fails in compiled ESM dist/ — fall back to the running entry point
|
|
496
|
+
let cliBin;
|
|
497
|
+
try {
|
|
498
|
+
cliBin = require.resolve('./index')
|
|
499
|
+
.replace(/\.ts$/, '.js')
|
|
500
|
+
.replace('/src/', '/dist/');
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
cliBin = process.argv[1];
|
|
504
|
+
}
|
|
505
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [cliBin, 'watch'], {
|
|
506
|
+
cwd: projectPath,
|
|
507
|
+
detached: true,
|
|
508
|
+
stdio: 'ignore',
|
|
509
|
+
});
|
|
510
|
+
child.unref();
|
|
511
|
+
return res.json({ ok: true, message: 'Watch daemon spawned' });
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
return res.status(500).json({ error: e.message });
|
|
515
|
+
}
|
|
516
|
+
});
|
|
362
517
|
// ── Static UI (must come after all /api/* routes) ─────────────────────
|
|
363
518
|
app.use(express_1.default.static(uiDist));
|
|
364
519
|
// SPA fallback — any unmatched route returns index.html so React Router
|
package/dist/index.js
CHANGED
|
@@ -130,6 +130,23 @@ else if (cmd === 'update' || cmd === 'upgrade') {
|
|
|
130
130
|
check: hasFlag('--check'),
|
|
131
131
|
force: hasFlag('--force'),
|
|
132
132
|
}));
|
|
133
|
+
// ── recent — show recent DevLoop task activity ────────────────────────────────
|
|
134
|
+
}
|
|
135
|
+
else if (cmd === 'recent') {
|
|
136
|
+
const daysFlag = getFlagValue('--days');
|
|
137
|
+
const fmt = getFlagValue('--format');
|
|
138
|
+
Promise.resolve().then(() => __importStar(require('./commands/recent'))).then(m => m.run({
|
|
139
|
+
days: daysFlag ? (isNaN(parseInt(daysFlag, 10)) ? 7 : parseInt(daysFlag, 10)) : 7,
|
|
140
|
+
format: fmt ?? 'table',
|
|
141
|
+
}));
|
|
142
|
+
// ── rewiki — re-ingest all sources with new extractor rules ──────────────────
|
|
143
|
+
}
|
|
144
|
+
else if (cmd === 'rewiki') {
|
|
145
|
+
Promise.resolve().then(() => __importStar(require('./commands/rewiki'))).then(m => m.run({
|
|
146
|
+
dryRun: hasFlag('--dry-run'),
|
|
147
|
+
noBackup: hasFlag('--no-backup'),
|
|
148
|
+
quiet: hasFlag('--quiet', '-q'),
|
|
149
|
+
}));
|
|
133
150
|
}
|
|
134
151
|
else {
|
|
135
152
|
console.log(`DocuFlow v${version}`);
|
|
@@ -175,6 +192,11 @@ else {
|
|
|
175
192
|
console.log(' update --check Check whether a newer version is published (no install)');
|
|
176
193
|
console.log(' update --force Reinstall even when already on the latest version');
|
|
177
194
|
console.log(' upgrade Alias for "update"');
|
|
195
|
+
console.log(' recent [--days N] [--format table|md] Show recent DevLoop task activity');
|
|
196
|
+
console.log(' rewiki Re-ingest all sources with current extractor rules (migration)');
|
|
197
|
+
console.log(' rewiki --dry-run Preview what would change without writing anything');
|
|
198
|
+
console.log(' rewiki --no-backup Skip wiki backup (faster, irreversible)');
|
|
199
|
+
console.log(' rewiki --quiet Suppress output (CI mode)');
|
|
178
200
|
console.log('');
|
|
179
201
|
console.log('Options:');
|
|
180
202
|
console.log(' --version, -v Print version number');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doquflow/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CLI for setting up Docuflow in your project",
|
|
5
5
|
"author": "Docuflow <hello@doquflows.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"build": "tsc && node -e \"const fs=require('fs'),p=require('path'),src=p.join(process.cwd(),'../ui/dist'),dst=p.join(process.cwd(),'ui-dist');if(!fs.existsSync(src)){console.log('Warning: packages/ui/dist not found — run npm run build:ui first');process.exit(0)}fs.mkdirSync(dst,{recursive:true});fs.cpSync(src,dst,{recursive:true,force:true});console.log(' ✓ ui-dist synced from packages/ui/dist ('+(fs.readdirSync(dst).length)+' files at root)')\""
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@doquflow/server": "1.
|
|
34
|
+
"@doquflow/server": "1.6.0",
|
|
35
35
|
"cors": "^2.8.5",
|
|
36
36
|
"express": "^4.19.2"
|
|
37
37
|
},
|