@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.
@@ -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/index.js CHANGED
@@ -45,157 +45,163 @@ function getFlagValue(flag) {
45
45
  const idx = rest.indexOf(flag);
46
46
  return idx !== -1 ? rest[idx + 1] : undefined;
47
47
  }
48
- if (cmd === '--version' || cmd === '-v') {
49
- console.log(version);
48
+ // ── hasFlagIn / getFlagValueIn scoped to a specific args array ─────────────
49
+ function hasFlagIn(arr, ...flags) {
50
+ return flags.some(f => arr.includes(f));
50
51
  }
51
- else if (cmd === 'init') {
52
- if (hasFlag('--interactive', '-i')) {
53
- Promise.resolve().then(() => __importStar(require('./commands/init-interactive'))).then(m => m.runInteractive());
52
+ function getFlagValueIn(arr, flag) {
53
+ const idx = arr.indexOf(flag);
54
+ return idx !== -1 ? arr[idx + 1] : undefined;
55
+ }
56
+ // ── dispatch — routes a (cmd, cmdRest) pair to the right handler ──────────────
57
+ function dispatch(c, r) {
58
+ if (c === '--version' || c === '-v') {
59
+ console.log(version);
60
+ // ── CORE ──────────────────────────────────────────────────────────────────
54
61
  }
55
- else {
56
- Promise.resolve().then(() => __importStar(require('./commands/init'))).then(m => m.run());
62
+ else if (c === 'init') {
63
+ if (hasFlagIn(r, '--interactive', '-i')) {
64
+ Promise.resolve().then(() => __importStar(require('./commands/init-interactive'))).then(m => m.runInteractive());
65
+ }
66
+ else {
67
+ Promise.resolve().then(() => __importStar(require('./commands/init'))).then(m => m.run());
68
+ }
57
69
  }
58
- }
59
- else if (cmd === 'status') {
60
- Promise.resolve().then(() => __importStar(require('./commands/status'))).then(m => m.run());
61
- }
62
- else if (cmd === 'suggest') {
63
- Promise.resolve().then(() => __importStar(require('./commands/suggest'))).then(m => m.run());
64
- // ── ui / start — web interface ───────────────────────────────────────────────
65
- }
66
- else if (cmd === 'ui' || cmd === 'start') {
67
- const portFlag = getFlagValue('--port');
68
- Promise.resolve().then(() => __importStar(require('./commands/ui'))).then(m => m.run({
69
- port: portFlag ? parseInt(portFlag, 10) : undefined,
70
- noOpen: hasFlag('--no-open'),
71
- }));
72
- // ── watch — auto-sync daemon ─────────────────────────────────────────────────
73
- }
74
- else if (cmd === 'watch') {
75
- const subCmd = rest[0];
76
- if (subCmd === 'stop') {
77
- Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runStop(process.cwd()));
70
+ else if (c === 'status') {
71
+ Promise.resolve().then(() => __importStar(require('./commands/status'))).then(m => m.run());
72
+ // ── query ask the wiki ──────────────────────────────────────────────────
73
+ }
74
+ else if (c === 'query') {
75
+ const question = r[0] && !r[0].startsWith('--') ? r[0] : '';
76
+ const maxSourcesFlag = getFlagValueIn(r, '--max-sources');
77
+ Promise.resolve().then(() => __importStar(require('./commands/query'))).then(m => m.run({
78
+ question,
79
+ maxSources: maxSourcesFlag ? parseInt(maxSourcesFlag, 10) : 5,
80
+ json: hasFlagIn(r, '--json'),
81
+ noCite: hasFlagIn(r, '--no-cite'),
82
+ saveAs: getFlagValueIn(r, '--save-as'),
83
+ quiet: hasFlagIn(r, '--quiet', '-q'),
84
+ }));
85
+ // ── ingest — ingest a source file into the wiki ───────────────────────────
78
86
  }
79
- else if (subCmd === 'status') {
80
- Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runStatus(process.cwd()));
87
+ else if (c === 'ingest') {
88
+ const sourceFile = r[0] && !r[0].startsWith('--') ? r[0] : undefined;
89
+ Promise.resolve().then(() => __importStar(require('./commands/ingest'))).then(m => m.run({
90
+ sourceFile,
91
+ all: hasFlagIn(r, '--all'),
92
+ dryRun: hasFlagIn(r, '--dry-run'),
93
+ quiet: hasFlagIn(r, '--quiet', '-q'),
94
+ }));
95
+ // ── rewiki — re-ingest all sources with new extractor rules ──────────────
81
96
  }
82
- else if (subCmd === 'restart') {
83
- Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runRestart(process.cwd()));
97
+ else if (c === 'rewiki') {
98
+ Promise.resolve().then(() => __importStar(require('./commands/rewiki'))).then(m => m.run({
99
+ dryRun: hasFlagIn(r, '--dry-run'),
100
+ noBackup: hasFlagIn(r, '--no-backup'),
101
+ quiet: hasFlagIn(r, '--quiet', '-q'),
102
+ }));
103
+ // ── ADVANCED ──────────────────────────────────────────────────────────────
84
104
  }
85
- else {
86
- const lintHours = getFlagValue('--lint-interval');
87
- const codeExt = getFlagValue('--code-ext');
88
- Promise.resolve().then(() => __importStar(require('./commands/watch'))).then(m => m.run({
89
- ai: hasFlag('--ai'),
90
- forceCopilot: hasFlag('--copilot'),
91
- forceClaude: hasFlag('--claude'),
92
- forceCodex: hasFlag('--codex'),
93
- lintIntervalHours: lintHours ? Number(lintHours) : 24,
94
- codeExtensions: codeExt ? codeExt.split(',') : undefined,
95
- allowDangerousPermissions: hasFlag('--allow-dangerous-permissions'),
105
+ else if (c === 'suggest') {
106
+ Promise.resolve().then(() => __importStar(require('./commands/suggest'))).then(m => m.run());
107
+ // ── ui / start — web interface ────────────────────────────────────────────
108
+ }
109
+ else if (c === 'ui' || c === 'start') {
110
+ const portFlag = getFlagValueIn(r, '--port');
111
+ Promise.resolve().then(() => __importStar(require('./commands/ui'))).then(m => m.run({
112
+ port: portFlag ? parseInt(portFlag, 10) : undefined,
113
+ noOpen: hasFlagIn(r, '--no-open'),
96
114
  }));
115
+ // ── watch — auto-sync daemon ──────────────────────────────────────────────
116
+ }
117
+ else if (c === 'watch') {
118
+ const subCmd = r[0];
119
+ if (subCmd === 'stop') {
120
+ Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runStop(process.cwd()));
121
+ }
122
+ else if (subCmd === 'status') {
123
+ Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runStatus(process.cwd()));
124
+ }
125
+ else if (subCmd === 'restart') {
126
+ Promise.resolve().then(() => __importStar(require('./commands/watch-stop'))).then(m => m.runRestart(process.cwd()));
127
+ }
128
+ else {
129
+ const lintHours = getFlagValueIn(r, '--lint-interval');
130
+ const codeExt = getFlagValueIn(r, '--code-ext');
131
+ Promise.resolve().then(() => __importStar(require('./commands/watch'))).then(m => m.run({
132
+ ai: hasFlagIn(r, '--ai'),
133
+ forceCopilot: hasFlagIn(r, '--copilot'),
134
+ forceClaude: hasFlagIn(r, '--claude'),
135
+ forceCodex: hasFlagIn(r, '--codex'),
136
+ lintIntervalHours: lintHours ? Number(lintHours) : 24,
137
+ codeExtensions: codeExt ? codeExt.split(',') : undefined,
138
+ allowDangerousPermissions: hasFlagIn(r, '--allow-dangerous-permissions'),
139
+ }));
140
+ }
141
+ // ── sync — one-shot sync for CI/CD and git hooks ──────────────────────────
142
+ }
143
+ else if (c === 'sync') {
144
+ const sinceCommit = getFlagValueIn(r, '--since-commit');
145
+ const sourceFile = getFlagValueIn(r, '--source');
146
+ const failScore = getFlagValueIn(r, '--fail-on-score');
147
+ Promise.resolve().then(() => __importStar(require('./commands/sync'))).then(m => m.run({
148
+ ai: hasFlagIn(r, '--ai'),
149
+ forceCopilot: hasFlagIn(r, '--copilot'),
150
+ forceClaude: hasFlagIn(r, '--claude'),
151
+ forceCodex: hasFlagIn(r, '--codex'),
152
+ sinceCommit,
153
+ sourceFile,
154
+ noLint: hasFlagIn(r, '--no-lint'),
155
+ failOnScore: failScore ? Number(failScore) : 70,
156
+ quiet: hasFlagIn(r, '--quiet', '-q'),
157
+ allowDangerousPermissions: hasFlagIn(r, '--allow-dangerous-permissions'),
158
+ }));
159
+ // ── review — git change review & improvement suggestions ──────────────────
160
+ }
161
+ else if (c === 'review') {
162
+ Promise.resolve().then(() => __importStar(require('./commands/review'))).then(m => m.run({
163
+ staged: hasFlagIn(r, '--staged'),
164
+ sinceCommit: getFlagValueIn(r, '--since-commit'),
165
+ ai: hasFlagIn(r, '--ai'),
166
+ failOnCritical: hasFlagIn(r, '--fail-on-critical'),
167
+ quiet: hasFlagIn(r, '--quiet', '-q'),
168
+ }));
169
+ // ── update — reinstall latest @doquflow/cli globally ─────────────────────
170
+ }
171
+ else if (c === 'update' || c === 'upgrade') {
172
+ Promise.resolve().then(() => __importStar(require('./commands/update'))).then(m => m.run({
173
+ check: hasFlagIn(r, '--check'),
174
+ force: hasFlagIn(r, '--force'),
175
+ }));
176
+ // ── recent — show recent task activity ────────────────────────────────────
177
+ }
178
+ else if (c === 'recent') {
179
+ const daysFlag = getFlagValueIn(r, '--days');
180
+ const fmt = getFlagValueIn(r, '--format');
181
+ Promise.resolve().then(() => __importStar(require('./commands/recent'))).then(m => m.run({
182
+ days: daysFlag ? (isNaN(parseInt(daysFlag, 10)) ? 7 : parseInt(daysFlag, 10)) : 7,
183
+ format: fmt ?? 'table',
184
+ }));
185
+ // ── unknown command ───────────────────────────────────────────────────────
186
+ }
187
+ else {
188
+ Promise.resolve().then(() => __importStar(require('./commands/help'))).then(m => m.printCoreHelp());
97
189
  }
98
- // ── sync — one-shot sync for CI/CD and git hooks ─────────────────────────────
99
- }
100
- else if (cmd === 'sync') {
101
- const sinceCommit = getFlagValue('--since-commit');
102
- const sourceFile = getFlagValue('--source');
103
- const failScore = getFlagValue('--fail-on-score');
104
- Promise.resolve().then(() => __importStar(require('./commands/sync'))).then(m => m.run({
105
- ai: hasFlag('--ai'),
106
- forceCopilot: hasFlag('--copilot'),
107
- forceClaude: hasFlag('--claude'),
108
- forceCodex: hasFlag('--codex'),
109
- sinceCommit,
110
- sourceFile,
111
- noLint: hasFlag('--no-lint'),
112
- failOnScore: failScore ? Number(failScore) : 70,
113
- quiet: hasFlag('--quiet', '-q'),
114
- allowDangerousPermissions: hasFlag('--allow-dangerous-permissions'),
115
- }));
116
- // ── review — git change review & improvement suggestions ───────────────────────
117
- }
118
- else if (cmd === 'review') {
119
- Promise.resolve().then(() => __importStar(require('./commands/review'))).then(m => m.run({
120
- staged: hasFlag('--staged'),
121
- sinceCommit: getFlagValue('--since-commit'),
122
- ai: hasFlag('--ai'),
123
- failOnCritical: hasFlag('--fail-on-critical'),
124
- quiet: hasFlag('--quiet', '-q'),
125
- }));
126
- // ── update — reinstall latest @doquflow/cli globally ─────────────────────────
127
190
  }
128
- else if (cmd === 'update' || cmd === 'upgrade') {
129
- Promise.resolve().then(() => __importStar(require('./commands/update'))).then(m => m.run({
130
- check: hasFlag('--check'),
131
- force: hasFlag('--force'),
132
- }));
133
- // ── recent — show recent DevLoop task activity ────────────────────────────────
191
+ // ── Entry point ───────────────────────────────────────────────────────────────
192
+ if (!cmd || cmd === '--help' || cmd === '-h') {
193
+ Promise.resolve().then(() => __importStar(require('./commands/help'))).then(m => m.printCoreHelp());
194
+ // ── advanced — optional prefix for the advanced surface ──────────────────────
134
195
  }
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
- }));
196
+ else if (cmd === 'advanced') {
197
+ const [advCmd, ...advRest] = rest;
198
+ if (!advCmd || advCmd === '--help' || advCmd === '-h') {
199
+ Promise.resolve().then(() => __importStar(require('./commands/help'))).then(m => m.printAdvancedHelp());
200
+ }
201
+ else {
202
+ dispatch(advCmd, advRest);
203
+ }
142
204
  }
143
205
  else {
144
- console.log(`DocuFlow v${version}`);
145
- console.log('');
146
- console.log('Usage: docuflow <command> [options]');
147
- console.log('');
148
- console.log('Commands:');
149
- console.log(' init Register DocuFlow MCP and generate CLAUDE.md');
150
- console.log(' init --interactive Interactive setup wizard');
151
- console.log(' status Show wiki health, page counts, and MCP status');
152
- console.log(' suggest Show what to document first (domain-specific)');
153
- console.log(' ui Start the DocuFlow web interface (API + UI on port 48821)');
154
- console.log(' start Alias for "ui" — same web interface');
155
- console.log(' ui --port <n> Use a custom port (default: 48821)');
156
- console.log(' ui --no-open Start server without auto-opening the browser');
157
- console.log(' watch Start auto-sync daemon (watches for changes)');
158
- console.log(' watch --ai Auto-detect best AI bridge (copilot > claude > codex > api)');
159
- console.log(' watch --ai --copilot Force @github/copilot CLI (direct MCP tool calling ⚡)');
160
- console.log(' watch --ai --claude Force Claude Code CLI (direct MCP tool calling ⚡)');
161
- console.log(' watch --ai --codex Force Codex CLI (generates doc → ingest)');
162
- console.log(' watch --lint-interval N Run lint every N hours (default: 24)');
163
- console.log(' watch --code-ext ts,py Watch only these file extensions');
164
- console.log(' watch stop Stop the running watch daemon for this project');
165
- console.log(' watch status Show daemon state: running/stopped, PID, uptime, bridge');
166
- console.log(' watch restart Stop current daemon and restart with same options');
167
- console.log(' sync One-shot sync: ingest all sources + rebuild index');
168
- console.log(' sync --ai AI-powered sync (auto-detects bridge)');
169
- console.log(' sync --ai --copilot Copilot drives DocuFlow MCP tools directly ⚡');
170
- console.log(' sync --ai --claude Claude drives DocuFlow MCP tools directly ⚡');
171
- console.log(' sync --ai --codex Codex generates doc → ingest');
172
- console.log(' sync --since-commit REF Diff code changes since git ref (e.g. HEAD~1)');
173
- console.log(' sync --source FILE Sync a single source file');
174
- console.log(' sync --no-lint Skip health check (faster)');
175
- console.log(' sync --fail-on-score N Exit 1 if health score < N (default: 70)');
176
- console.log(' sync --quiet Suppress output (CI mode)');
177
- console.log(' review Review current git changes and suggest improvements');
178
- console.log(' review --staged Review staged changes only');
179
- console.log(' review --since-commit REF Review changes since git ref (e.g. HEAD~1)');
180
- console.log(' review --ai Append Copilot AI review to deterministic findings');
181
- console.log(' review --fail-on-critical Exit 1 if critical findings are detected');
182
- console.log(' review --quiet Compact output for CI/scripting');
183
- console.log(' update Reinstall latest @doquflow/cli globally (refreshes UI + server)');
184
- console.log(' update --check Check whether a newer version is published (no install)');
185
- console.log(' update --force Reinstall even when already on the latest version');
186
- console.log(' upgrade Alias for "update"');
187
- console.log(' recent [--days N] [--format table|md] Show recent DevLoop task activity');
188
- console.log('');
189
- console.log('Options:');
190
- console.log(' --version, -v Print version number');
191
- console.log(' --allow-dangerous-permissions Pass --dangerously-skip-permissions to Claude CLI');
192
- console.log(' Required for Claude bridge in non-interactive use.');
193
- console.log(' Only use when file content in this project is trusted.');
194
- console.log('');
195
- console.log('AI bridge priority (for --ai flag):');
196
- console.log(' 1. copilot (@github/copilot) — calls DocuFlow MCP tools directly ⚡');
197
- console.log(' 2. claude (Claude Code CLI) — calls DocuFlow MCP tools directly ⚡');
198
- console.log(' 3. codex (OpenAI Codex CLI) — generates doc text, then ingests');
199
- console.log(' 4. api (ANTHROPIC_API_KEY env) — generates doc text, then ingests');
200
- console.log(' Use --copilot / --claude / --codex to override auto-detection.');
206
+ dispatch(cmd, rest);
201
207
  }