@doquflow/cli 1.5.0 → 1.5.2

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.
@@ -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,15 @@ 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
+ }));
133
142
  }
134
143
  else {
135
144
  console.log(`DocuFlow v${version}`);
@@ -175,6 +184,7 @@ else {
175
184
  console.log(' update --check Check whether a newer version is published (no install)');
176
185
  console.log(' update --force Reinstall even when already on the latest version');
177
186
  console.log(' upgrade Alias for "update"');
187
+ console.log(' recent [--days N] [--format table|md] Show recent DevLoop task activity');
178
188
  console.log('');
179
189
  console.log('Options:');
180
190
  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.5.0",
3
+ "version": "1.5.2",
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.5.0",
34
+ "@doquflow/server": "1.5.2",
35
35
  "cors": "^2.8.5",
36
36
  "express": "^4.19.2"
37
37
  },