@doquflow/cli 1.3.0 → 1.3.1

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.
@@ -137,6 +137,31 @@ async function runStatus(projectPath) {
137
137
  console.log(` Uptime: ${formatUptime(data.started_at)}`);
138
138
  console.log(` Started: ${new Date(data.started_at).toLocaleString()}`);
139
139
  console.log(` Bridge: ${bridgeLabel}`);
140
+ // Active bridge — only shown when it differs from startup bridge (failover occurred)
141
+ const activeBridge = data.active_bridge ?? data.bridge;
142
+ if (activeBridge !== data.bridge) {
143
+ const activeBridgeLabel = activeBridge === "none" ? c.dim("sources-only (no AI)") :
144
+ activeBridge === "copilot" ? c.green("copilot — direct MCP ⚡") :
145
+ activeBridge === "claude" ? c.green("claude — direct MCP ⚡") :
146
+ activeBridge === "codex" ? c.yellow("codex — doc-gen mode") :
147
+ activeBridge === "api" ? c.yellow("api — doc-gen mode") : activeBridge;
148
+ console.log(` Active bridge: ${activeBridgeLabel} ${c.yellow("(failed over)")}`);
149
+ }
150
+ // Failover stats
151
+ const failover = data.failover ?? { count: 0, last_at: null, from: null, to: null, reason: null };
152
+ if (failover.count > 0) {
153
+ const lastAt = failover.last_at
154
+ ? new Date(failover.last_at).toLocaleTimeString()
155
+ : "unknown";
156
+ console.log(` Failovers: ${c.yellow(String(failover.count))} total`);
157
+ console.log(` Last failover: ${lastAt} — ${failover.from} → ${failover.to}`);
158
+ if (failover.reason) {
159
+ console.log(` Reason: ${c.dim(failover.reason)}`);
160
+ }
161
+ }
162
+ else {
163
+ console.log(` Failovers: ${c.dim("none")}`);
164
+ }
140
165
  console.log(` Project: ${data.project_path}`);
141
166
  if (data.options.lintIntervalHours) {
142
167
  console.log(` Lint: every ${data.options.lintIntervalHours}h`);
@@ -39,6 +39,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  };
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.detectBridge = detectBridge;
42
+ exports.getNextBridge = getNextBridge;
43
+ exports.recordFailover = recordFailover;
42
44
  exports.getPidFilePath = getPidFilePath;
43
45
  exports.writePidFile = writePidFile;
44
46
  exports.removePidFile = removePidFile;
@@ -282,7 +284,11 @@ function buildDocGenPrompt(projectPath, changedFiles) {
282
284
  ].join("\n");
283
285
  }
284
286
  // ─── Core sync dispatcher ─────────────────────────────────────────────────────
285
- async function syncWithAI(projectPath, changedFiles, bridge) {
287
+ async function syncWithAI(projectPath, changedFiles, bridge, depth = 0) {
288
+ if (bridge === "none" || depth >= 4) {
289
+ await directIngestAll(projectPath);
290
+ return;
291
+ }
286
292
  const bridgeLabel = bridge === "copilot" ? "Copilot" : bridge === "claude" ? "Claude" : bridge === "codex" ? "Codex" : "API";
287
293
  log("🤖", `${changedFiles.length} file(s) changed — asking ${c.cyan(bridgeLabel)} to update wiki...`);
288
294
  // Copilot and Claude: DIRECT MCP tool calling
@@ -294,8 +300,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
294
300
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
295
301
  }
296
302
  else {
297
- log("⚠️ ", c.yellow("Copilot returned no result — falling back to direct ingest"));
298
- await directIngestAll(projectPath);
303
+ const next = getNextBridge(bridge);
304
+ await recordFailover(projectPath, bridge, next, "no output");
305
+ log("⚠️ ", c.yellow(`Copilot failed — falling over to ${next}`));
306
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
299
307
  }
300
308
  return;
301
309
  }
@@ -307,8 +315,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
307
315
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
308
316
  }
309
317
  else {
310
- log("⚠️ ", c.yellow("Claude returned no result — falling back to direct ingest"));
311
- await directIngestAll(projectPath);
318
+ const next = getNextBridge(bridge);
319
+ await recordFailover(projectPath, bridge, next, "no output");
320
+ log("⚠️ ", c.yellow(`Claude failed — falling over to ${next}`));
321
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
312
322
  }
313
323
  return;
314
324
  }
@@ -322,8 +332,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
322
332
  docContent = await callAnthropicAPI(docPrompt);
323
333
  }
324
334
  if (!docContent) {
325
- log("⚠️ ", c.yellow("AI returned no content — falling back to direct ingest"));
326
- await directIngestAll(projectPath);
335
+ const next = getNextBridge(bridge);
336
+ await recordFailover(projectPath, bridge, next, "no output");
337
+ log("⚠️ ", c.yellow(`${bridgeLabel} failed — falling over to ${next}`));
338
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
327
339
  return;
328
340
  }
329
341
  // Save AI-generated doc to sources/
@@ -335,13 +347,23 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
335
347
  log("💾", `AI doc saved → ${c.cyan(autoFilename)}`);
336
348
  await directIngest(projectPath, autoFilename);
337
349
  }
338
- async function scheduledLintWithAI(projectPath, bridge) {
350
+ async function scheduledLintWithAI(projectPath, bridge, depth = 0) {
351
+ if (bridge === "none" || depth >= 4) {
352
+ // Lint is best-effort — silent return at terminal level (no directIngestAll)
353
+ return;
354
+ }
339
355
  if (bridge === "copilot") {
340
356
  log("🔍", `Running scheduled lint via ${c.cyan("Copilot")} (direct MCP call)...`);
341
357
  const result = runCopilotCLI(buildCopilotLintPrompt(projectPath));
342
358
  if (result) {
343
359
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
344
360
  }
361
+ else {
362
+ const next = getNextBridge(bridge);
363
+ await recordFailover(projectPath, bridge, next, "no output during lint");
364
+ log("⚠️ ", c.yellow(`Copilot lint failed — falling over to ${next}`));
365
+ await scheduledLintWithAI(projectPath, next, depth + 1);
366
+ }
345
367
  return;
346
368
  }
347
369
  if (bridge === "claude") {
@@ -350,10 +372,17 @@ async function scheduledLintWithAI(projectPath, bridge) {
350
372
  if (result) {
351
373
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
352
374
  }
375
+ else {
376
+ const next = getNextBridge(bridge);
377
+ await recordFailover(projectPath, bridge, next, "no output during lint");
378
+ log("⚠️ ", c.yellow(`Claude lint failed — falling over to ${next}`));
379
+ await scheduledLintWithAI(projectPath, next, depth + 1);
380
+ }
353
381
  return;
354
382
  }
355
- // Fallback: call lint tool directly
356
- await directLint(projectPath);
383
+ const next = getNextBridge(bridge);
384
+ log("ℹ️ ", c.dim(`${bridge} cannot perform MCP lint — escalating to ${next}`));
385
+ await scheduledLintWithAI(projectPath, next, depth + 1);
357
386
  }
358
387
  // ─── Direct tool calls (no AI) ───────────────────────────────────────────────
359
388
  async function directIngest(projectPath, filename) {
@@ -378,22 +407,6 @@ async function directIngestAll(projectPath) {
378
407
  }
379
408
  catch { }
380
409
  }
381
- async function directLint(projectPath) {
382
- const { lintWiki } = loadServerTool("lint-wiki");
383
- log("🔍", "Running scheduled lint check...");
384
- const result = await lintWiki({ project_path: projectPath, check_type: "all" });
385
- const score = result.health_score ?? 0;
386
- const scoreLabel = score >= 90 ? c.green(`${score}/100`) : score >= 70 ? c.yellow(`${score}/100`) : c.red(`${score}/100`);
387
- log("📊", `Health score: ${scoreLabel}`);
388
- if (result.issues_found?.length > 0) {
389
- const high = result.issues_found.filter((i) => i.severity === "high").length;
390
- const med = result.issues_found.filter((i) => i.severity === "medium").length;
391
- log("⚠️ ", `Issues: 🔴 ${high} high 🟡 ${med} medium`);
392
- for (const rec of result.recommendations?.slice(0, 3) ?? []) {
393
- console.log(c.dim(` → ${rec}`));
394
- }
395
- }
396
- }
397
410
  // ─── Debounce helper ─────────────────────────────────────────────────────────
398
411
  function debounce(fn, ms) {
399
412
  let timer;
@@ -408,6 +421,32 @@ const DEFAULT_CODE_EXTS = new Set([
408
421
  ".py", ".go", ".rb", ".java", ".cs",
409
422
  ".php", ".rs", ".kt", ".swift", ".vue",
410
423
  ]);
424
+ function getNextBridge(current) {
425
+ const chain = ["copilot", "claude", "codex", "api", "none"];
426
+ const idx = chain.indexOf(current);
427
+ return (idx !== -1 && idx + 1 < chain.length) ? chain[idx + 1] : "none";
428
+ }
429
+ async function recordFailover(projectPath, from, to, reason) {
430
+ try {
431
+ const data = await readPidFile(projectPath);
432
+ if (!data)
433
+ return;
434
+ if (!data.failover) {
435
+ data.failover = { count: 0, last_at: null, from: null, to: null, reason: null };
436
+ }
437
+ data.failover.count += 1;
438
+ data.failover.last_at = new Date().toISOString();
439
+ data.failover.from = from;
440
+ data.failover.to = to;
441
+ data.failover.reason = reason.slice(0, 120);
442
+ data.active_bridge = to;
443
+ try {
444
+ await writePidFile(projectPath, data);
445
+ }
446
+ catch { }
447
+ }
448
+ catch { }
449
+ }
411
450
  function getPidFilePath(projectPath) {
412
451
  return node_path_1.default.join(projectPath, ".docuflow", "watch.pid");
413
452
  }
@@ -501,6 +540,7 @@ async function run(options = {}) {
501
540
  pid: process.pid,
502
541
  started_at: new Date().toISOString(),
503
542
  bridge,
543
+ active_bridge: bridge,
504
544
  project_path: projectPath,
505
545
  options: {
506
546
  ai: !!options.ai,
@@ -510,6 +550,13 @@ async function run(options = {}) {
510
550
  lintIntervalHours: options.lintIntervalHours ?? 24,
511
551
  codeExtensions: options.codeExtensions,
512
552
  },
553
+ failover: {
554
+ count: 0,
555
+ last_at: null,
556
+ from: null,
557
+ to: null,
558
+ reason: null,
559
+ },
513
560
  });
514
561
  log("💾", `PID ${process.pid} written to ${c.dim(".docuflow/watch.pid")}`);
515
562
  // ── Watch 1: .docuflow/sources/ ─────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doquflow/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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.3.0",
34
+ "@doquflow/server": "1.3.1",
35
35
  "cors": "^2.8.5",
36
36
  "express": "^4.19.2"
37
37
  },