@doquflow/cli 1.2.1 → 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.
- package/dist/commands/review.js +331 -0
- package/dist/commands/watch-stop.js +25 -0
- package/dist/commands/watch.js +73 -26
- package/dist/index.js +16 -0
- package/package.json +2 -2
- package/ui-dist/assets/index-3-icxZEZ.css +1 -0
- package/ui-dist/assets/index-B5BRczn1.js +44 -0
- package/ui-dist/assets/index-BWcDlOkO.css +1 -0
- package/ui-dist/assets/index-C78tFlFj.css +1 -0
- package/ui-dist/assets/index-C98_cjC3.js +44 -0
- package/ui-dist/assets/index-CRwiGLWM.js +44 -0
- package/ui-dist/assets/index-ChWtqBFK.js +44 -0
- package/ui-dist/assets/index-T8ZPx9-_.css +1 -0
- package/ui-dist/assets/index-Wb1LwUpP.js +45 -0
- package/ui-dist/index.html +2 -2
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docuflow review
|
|
4
|
+
*
|
|
5
|
+
* Review git changes and surface deterministic findings plus actionable
|
|
6
|
+
* improvements. Optional --ai mode appends Copilot analysis.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getChangedFiles = getChangedFiles;
|
|
13
|
+
exports.getDiffText = getDiffText;
|
|
14
|
+
exports.buildDeterministicReview = buildDeterministicReview;
|
|
15
|
+
exports.runCopilotReview = runCopilotReview;
|
|
16
|
+
exports.run = run;
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_child_process_1 = require("node:child_process");
|
|
19
|
+
const DIFF_CAP_BYTES = 200 * 1024;
|
|
20
|
+
const AI_DIFF_CAP_CHARS = 40_000;
|
|
21
|
+
const c = {
|
|
22
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
23
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
24
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
25
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
26
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
28
|
+
};
|
|
29
|
+
function runGit(projectPath, args) {
|
|
30
|
+
const result = (0, node_child_process_1.spawnSync)("git", args, { cwd: projectPath, encoding: "utf8" });
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
const stderr = (result.stderr ?? "").trim();
|
|
33
|
+
const badRef = /bad revision|unknown revision|ambiguous argument/i.test(stderr);
|
|
34
|
+
if (badRef) {
|
|
35
|
+
throw new Error(`Invalid git ref: ${args.join(" ")}`);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(stderr || `git ${args.join(" ")} failed`);
|
|
38
|
+
}
|
|
39
|
+
return (result.stdout ?? "").trim();
|
|
40
|
+
}
|
|
41
|
+
function runGitAllowStatuses(projectPath, args, allowedStatuses) {
|
|
42
|
+
const result = (0, node_child_process_1.spawnSync)("git", args, { cwd: projectPath, encoding: "utf8" });
|
|
43
|
+
if (result.status === null || !allowedStatuses.includes(result.status)) {
|
|
44
|
+
const stderr = (result.stderr ?? "").trim();
|
|
45
|
+
const badRef = /bad revision|unknown revision|ambiguous argument/i.test(stderr);
|
|
46
|
+
if (badRef) {
|
|
47
|
+
throw new Error(`Invalid git ref: ${args.join(" ")}`);
|
|
48
|
+
}
|
|
49
|
+
throw new Error(stderr || `git ${args.join(" ")} failed`);
|
|
50
|
+
}
|
|
51
|
+
return (result.stdout ?? "").trim();
|
|
52
|
+
}
|
|
53
|
+
function ensureGitRepo(projectPath) {
|
|
54
|
+
try {
|
|
55
|
+
const out = runGit(projectPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
56
|
+
if (out !== "true") {
|
|
57
|
+
throw new Error("not a git repository");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`No git repository detected at ${projectPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function parsePorcelainPaths(porcelain) {
|
|
65
|
+
const out = new Set();
|
|
66
|
+
for (const line of porcelain.split("\n")) {
|
|
67
|
+
const trimmed = line.trim();
|
|
68
|
+
if (!trimmed)
|
|
69
|
+
continue;
|
|
70
|
+
const payload = line.replace(/^[ MARCUD?!]{1,2}\s+/, "").trim();
|
|
71
|
+
if (!payload)
|
|
72
|
+
continue;
|
|
73
|
+
if (payload.includes(" -> ")) {
|
|
74
|
+
const parts = payload.split(" -> ");
|
|
75
|
+
out.add(parts[parts.length - 1].trim());
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
out.add(payload);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
|
82
|
+
}
|
|
83
|
+
function capDiff(diffText) {
|
|
84
|
+
const bytes = Buffer.byteLength(diffText, "utf8");
|
|
85
|
+
if (bytes <= DIFF_CAP_BYTES)
|
|
86
|
+
return diffText;
|
|
87
|
+
const truncated = Buffer.from(diffText, "utf8").subarray(0, DIFF_CAP_BYTES).toString("utf8");
|
|
88
|
+
return `${truncated}\n\n[DOCUFLOW_DIFF_TRUNCATED: analyzed first ${DIFF_CAP_BYTES} bytes of ${bytes} bytes]\n`;
|
|
89
|
+
}
|
|
90
|
+
function pushUnique(arr, item) {
|
|
91
|
+
if (!arr.includes(item))
|
|
92
|
+
arr.push(item);
|
|
93
|
+
}
|
|
94
|
+
function getChangedFiles(projectPath, staged, sinceCommit) {
|
|
95
|
+
if (sinceCommit) {
|
|
96
|
+
const output = runGit(projectPath, ["diff", "--name-only", sinceCommit, "HEAD"]);
|
|
97
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
98
|
+
}
|
|
99
|
+
if (staged) {
|
|
100
|
+
const output = runGit(projectPath, ["diff", "--name-only", "--cached"]);
|
|
101
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
102
|
+
}
|
|
103
|
+
return parsePorcelainPaths(runGit(projectPath, ["status", "--porcelain"]));
|
|
104
|
+
}
|
|
105
|
+
function getDiffText(projectPath, staged, sinceCommit) {
|
|
106
|
+
if (sinceCommit) {
|
|
107
|
+
return capDiff(runGit(projectPath, ["diff", sinceCommit, "HEAD"]));
|
|
108
|
+
}
|
|
109
|
+
if (staged) {
|
|
110
|
+
return capDiff(runGit(projectPath, ["diff", "--cached"]));
|
|
111
|
+
}
|
|
112
|
+
const untracked = runGit(projectPath, ["ls-files", "--others", "--exclude-standard"])
|
|
113
|
+
.split("\n")
|
|
114
|
+
.map(line => line.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
const untrackedPatches = [];
|
|
117
|
+
for (const file of untracked) {
|
|
118
|
+
const patch = runGitAllowStatuses(projectPath, ["diff", "--no-index", "--", "/dev/null", file], [0, 1]);
|
|
119
|
+
if (patch) {
|
|
120
|
+
untrackedPatches.push(patch);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const stagedDiff = runGit(projectPath, ["diff", "--cached"]);
|
|
124
|
+
const workingDiff = runGit(projectPath, ["diff"]);
|
|
125
|
+
const joined = [
|
|
126
|
+
"=== STAGED DIFF ===",
|
|
127
|
+
stagedDiff,
|
|
128
|
+
"",
|
|
129
|
+
"=== WORKING TREE DIFF ===",
|
|
130
|
+
workingDiff,
|
|
131
|
+
"",
|
|
132
|
+
"=== UNTRACKED FILE DIFF ===",
|
|
133
|
+
untrackedPatches.join("\n"),
|
|
134
|
+
].join("\n");
|
|
135
|
+
return capDiff(joined);
|
|
136
|
+
}
|
|
137
|
+
function buildDeterministicReview(changedFiles, diffText) {
|
|
138
|
+
const summary = [];
|
|
139
|
+
const critical = [];
|
|
140
|
+
const warnings = [];
|
|
141
|
+
const improvements = [];
|
|
142
|
+
summary.push(`Changed files: ${changedFiles.length}`);
|
|
143
|
+
for (const f of changedFiles.slice(0, 10)) {
|
|
144
|
+
summary.push(f);
|
|
145
|
+
}
|
|
146
|
+
if (changedFiles.length > 10) {
|
|
147
|
+
summary.push(`...and ${changedFiles.length - 10} more`);
|
|
148
|
+
}
|
|
149
|
+
const truncated = diffText.includes("[DOCUFLOW_DIFF_TRUNCATED:");
|
|
150
|
+
if (truncated) {
|
|
151
|
+
warnings.push("Large diff detected; review is partial due to diff size cap.");
|
|
152
|
+
improvements.push("Run focused reviews per file or per commit range for full coverage.");
|
|
153
|
+
}
|
|
154
|
+
const addedLines = diffText
|
|
155
|
+
.split("\n")
|
|
156
|
+
.filter(line => line.startsWith("+") && !line.startsWith("+++"))
|
|
157
|
+
.map(line => line.slice(1));
|
|
158
|
+
if (changedFiles.length > 0 && addedLines.length === 0) {
|
|
159
|
+
summary.push("Textual diff is empty (likely binary/rename-only changes).");
|
|
160
|
+
improvements.push("Manually review binary assets and metadata changes before commit.");
|
|
161
|
+
return { summary, critical, warnings, improvements };
|
|
162
|
+
}
|
|
163
|
+
const addedText = addedLines.join("\n");
|
|
164
|
+
if (/(AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{20,}|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----)/.test(addedText)) {
|
|
165
|
+
critical.push("Potential secret/key material detected in added lines.");
|
|
166
|
+
improvements.push("Remove credentials from code and load secrets via environment variables.");
|
|
167
|
+
}
|
|
168
|
+
if (/(password|token|secret)\s*[:=]\s*["'][^"']{4,}["']/i.test(addedText)) {
|
|
169
|
+
critical.push("Hardcoded credential-like value detected.");
|
|
170
|
+
}
|
|
171
|
+
if (/\b(eval|new Function)\s*\(/.test(addedText)) {
|
|
172
|
+
critical.push("Dynamic code execution (`eval`/`new Function`) detected.");
|
|
173
|
+
improvements.push("Replace dynamic execution with explicit parsing or whitelisted dispatch.");
|
|
174
|
+
}
|
|
175
|
+
if (/\bchild_process\.(exec|execSync)\s*\(/.test(addedText)) {
|
|
176
|
+
warnings.push("Shell execution added via child_process.exec/execSync.");
|
|
177
|
+
improvements.push("Prefer spawn/spawnSync with argument arrays and explicit input validation.");
|
|
178
|
+
}
|
|
179
|
+
if (/\bTODO\b|\bFIXME\b|\bHACK\b/.test(addedText)) {
|
|
180
|
+
warnings.push("TODO/FIXME/HACK markers found in added code.");
|
|
181
|
+
improvements.push("Resolve or ticket these markers before merge to avoid hidden follow-up work.");
|
|
182
|
+
}
|
|
183
|
+
if (/console\.log\s*\(/.test(addedText)) {
|
|
184
|
+
warnings.push("console.log statements found in added lines.");
|
|
185
|
+
improvements.push("Use structured logger patterns or remove debug logging before merge.");
|
|
186
|
+
}
|
|
187
|
+
if (/\bas any\b|:\s*any\b/.test(addedText)) {
|
|
188
|
+
warnings.push("Type safety weakened with `any` usage.");
|
|
189
|
+
improvements.push("Tighten types with explicit interfaces or narrow unions.");
|
|
190
|
+
}
|
|
191
|
+
if (/@ts-ignore/.test(addedText)) {
|
|
192
|
+
warnings.push("@ts-ignore found in added lines.");
|
|
193
|
+
improvements.push("Replace @ts-ignore with proper typing or guarded runtime checks.");
|
|
194
|
+
}
|
|
195
|
+
if (/catch\s*(\([^)]*\))?\s*\{\s*\}/.test(addedText)) {
|
|
196
|
+
warnings.push("Empty catch block detected.");
|
|
197
|
+
improvements.push("Handle caught errors explicitly and emit actionable context.");
|
|
198
|
+
}
|
|
199
|
+
const longLines = addedLines.filter(line => line.length > 160).length;
|
|
200
|
+
if (longLines > 0) {
|
|
201
|
+
warnings.push(`${longLines} long added line(s) (>160 chars) may hurt readability.`);
|
|
202
|
+
}
|
|
203
|
+
const hasTestFile = changedFiles.some(f => /(^|\/)(test|tests|__tests__)\/|\.test\.[a-z]+$|\.spec\.[a-z]+$/i.test(f));
|
|
204
|
+
if (!hasTestFile) {
|
|
205
|
+
improvements.push("Consider adding or updating tests for changed behavior.");
|
|
206
|
+
}
|
|
207
|
+
if (critical.length === 0 && warnings.length === 0) {
|
|
208
|
+
summary.push("No deterministic critical/warning findings detected.");
|
|
209
|
+
improvements.push("Quick manual pass for architecture consistency and naming is still recommended.");
|
|
210
|
+
}
|
|
211
|
+
return { summary, critical, warnings, improvements };
|
|
212
|
+
}
|
|
213
|
+
function buildCopilotPrompt(projectPath, changedFiles, diffText) {
|
|
214
|
+
const diffForPrompt = diffText.slice(0, AI_DIFF_CAP_CHARS);
|
|
215
|
+
return [
|
|
216
|
+
`Review these git changes for project: ${projectPath}`,
|
|
217
|
+
``,
|
|
218
|
+
`Changed files:`,
|
|
219
|
+
changedFiles.slice(0, 40).map(f => `- ${f}`).join("\n") || "(none)",
|
|
220
|
+
``,
|
|
221
|
+
`Diff:`,
|
|
222
|
+
diffForPrompt || "(no textual diff)",
|
|
223
|
+
``,
|
|
224
|
+
`Return concise markdown with sections:`,
|
|
225
|
+
`1) Critical issues`,
|
|
226
|
+
`2) Warnings`,
|
|
227
|
+
`3) Concrete improvements`,
|
|
228
|
+
`Focus on correctness, security, and maintainability.`,
|
|
229
|
+
].join("\n");
|
|
230
|
+
}
|
|
231
|
+
function runCopilotReview(prompt) {
|
|
232
|
+
const result = (0, node_child_process_1.spawnSync)("copilot", [
|
|
233
|
+
"--prompt", prompt,
|
|
234
|
+
"--allow-all-tools",
|
|
235
|
+
"--allow-all-paths",
|
|
236
|
+
"--no-ask-user",
|
|
237
|
+
"--output-format", "json"
|
|
238
|
+
], { encoding: "utf8", timeout: 180_000 });
|
|
239
|
+
if (result.error || result.status !== 0)
|
|
240
|
+
return null;
|
|
241
|
+
let lastMessage = null;
|
|
242
|
+
for (const line of (result.stdout ?? "").split("\n")) {
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (!trimmed)
|
|
245
|
+
continue;
|
|
246
|
+
try {
|
|
247
|
+
const obj = JSON.parse(trimmed);
|
|
248
|
+
if (obj.type === "assistant.message" && obj.data?.content) {
|
|
249
|
+
lastMessage = obj.data.content;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// ignore malformed json lines
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return lastMessage;
|
|
257
|
+
}
|
|
258
|
+
function printSection(title, lines, quiet) {
|
|
259
|
+
const label = quiet ? title : c.bold(title);
|
|
260
|
+
console.log(`\n${label}`);
|
|
261
|
+
if (lines.length === 0) {
|
|
262
|
+
console.log(" - none");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
console.log(` - ${line}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function run(options = {}) {
|
|
270
|
+
const projectPath = node_path_1.default.resolve(options.projectPath ?? process.cwd());
|
|
271
|
+
const staged = !!options.staged;
|
|
272
|
+
const sinceCommit = options.sinceCommit;
|
|
273
|
+
const quiet = !!options.quiet;
|
|
274
|
+
const failOnCritical = !!options.failOnCritical;
|
|
275
|
+
const ai = !!options.ai;
|
|
276
|
+
try {
|
|
277
|
+
ensureGitRepo(projectPath);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error(c.red(`✗ ${error.message}`));
|
|
281
|
+
process.exit(2);
|
|
282
|
+
}
|
|
283
|
+
let changedFiles = [];
|
|
284
|
+
let diffText = "";
|
|
285
|
+
try {
|
|
286
|
+
changedFiles = getChangedFiles(projectPath, staged, sinceCommit);
|
|
287
|
+
diffText = getDiffText(projectPath, staged, sinceCommit);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const message = error.message;
|
|
291
|
+
if (message.startsWith("Invalid git ref:")) {
|
|
292
|
+
console.error(c.red(`✗ Invalid --since-commit ref: ${sinceCommit}`));
|
|
293
|
+
process.exit(2);
|
|
294
|
+
}
|
|
295
|
+
console.error(c.red(`✗ ${message}`));
|
|
296
|
+
process.exit(2);
|
|
297
|
+
}
|
|
298
|
+
if (changedFiles.length === 0) {
|
|
299
|
+
console.log(quiet ? "nothing to review" : c.cyan("Nothing to review: no changed files in selected scope."));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const modeLabel = sinceCommit
|
|
303
|
+
? `since ${sinceCommit}`
|
|
304
|
+
: staged
|
|
305
|
+
? "staged changes"
|
|
306
|
+
: "working tree (staged + unstaged)";
|
|
307
|
+
if (!quiet) {
|
|
308
|
+
console.log(c.bold("\n🔍 DocuFlow Review\n"));
|
|
309
|
+
console.log(`Scope: ${c.cyan(modeLabel)}`);
|
|
310
|
+
}
|
|
311
|
+
const deterministic = buildDeterministicReview(changedFiles, diffText);
|
|
312
|
+
printSection("Summary", deterministic.summary, quiet);
|
|
313
|
+
printSection("Critical", deterministic.critical, quiet);
|
|
314
|
+
printSection("Warnings", deterministic.warnings, quiet);
|
|
315
|
+
printSection("Improvements", deterministic.improvements, quiet);
|
|
316
|
+
if (ai) {
|
|
317
|
+
const prompt = buildCopilotPrompt(projectPath, changedFiles, diffText);
|
|
318
|
+
const aiResult = runCopilotReview(prompt);
|
|
319
|
+
if (aiResult) {
|
|
320
|
+
const aiLabel = quiet ? "AI Review (Copilot)" : c.bold("\nAI Review (Copilot)");
|
|
321
|
+
console.log(`\n${aiLabel}`);
|
|
322
|
+
console.log(aiResult);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
deterministic.improvements.push("Copilot AI review unavailable; using deterministic review only");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (failOnCritical && deterministic.critical.length > 0) {
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -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`);
|
package/dist/commands/watch.js
CHANGED
|
@@ -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
|
-
|
|
298
|
-
await
|
|
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
|
-
|
|
311
|
-
await
|
|
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
|
-
|
|
326
|
-
await
|
|
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
|
-
|
|
356
|
-
|
|
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/dist/index.js
CHANGED
|
@@ -113,6 +113,16 @@ else if (cmd === 'sync') {
|
|
|
113
113
|
quiet: hasFlag('--quiet', '-q'),
|
|
114
114
|
allowDangerousPermissions: hasFlag('--allow-dangerous-permissions'),
|
|
115
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
|
+
}));
|
|
116
126
|
}
|
|
117
127
|
else {
|
|
118
128
|
console.log(`DocuFlow v${version}`);
|
|
@@ -148,6 +158,12 @@ else {
|
|
|
148
158
|
console.log(' sync --no-lint Skip health check (faster)');
|
|
149
159
|
console.log(' sync --fail-on-score N Exit 1 if health score < N (default: 70)');
|
|
150
160
|
console.log(' sync --quiet Suppress output (CI mode)');
|
|
161
|
+
console.log(' review Review current git changes and suggest improvements');
|
|
162
|
+
console.log(' review --staged Review staged changes only');
|
|
163
|
+
console.log(' review --since-commit REF Review changes since git ref (e.g. HEAD~1)');
|
|
164
|
+
console.log(' review --ai Append Copilot AI review to deterministic findings');
|
|
165
|
+
console.log(' review --fail-on-critical Exit 1 if critical findings are detected');
|
|
166
|
+
console.log(' review --quiet Compact output for CI/scripting');
|
|
151
167
|
console.log('');
|
|
152
168
|
console.log('Options:');
|
|
153
169
|
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.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.
|
|
34
|
+
"@doquflow/server": "1.3.1",
|
|
35
35
|
"cors": "^2.8.5",
|
|
36
36
|
"express": "^4.19.2"
|
|
37
37
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--df-bg: #0a0a0b;--df-surface: #111113;--df-surface-2: #161619;--df-surface-3: #1d1d22;--df-border: #1a1a1f;--df-border-2: #26262c;--df-border-3: #2c2c33;--df-border-hi: #3a3a44;--df-text: #ededee;--df-text-2: #d4d4d8;--df-text-3: #a1a1aa;--df-text-4: #6b6b74;--df-accent: #6366f1;--df-accent-hover: #5558e3;--df-accent-2: #818cf8;--df-accent-text: #a5b4fc;--df-accent-soft: rgba(99,102,241,.14);--df-accent-glow: rgba(99,102,241,.18);--df-accent-ring: rgba(99,102,241,.4);--df-accent-halo: rgba(99,102,241,.08);--df-green: #10b981;--df-green-text: #34d399;--df-green-soft: rgba(16,185,129,.12);--df-green-ring: rgba(16,185,129,.3);--df-amber: #f59e0b;--df-amber-text: #fbbf24;--df-amber-soft: rgba(245,158,11,.12);--df-amber-ring: rgba(245,158,11,.3);--df-red: #ef4444;--df-red-text: #f87171;--df-red-soft: rgba(239,68,68,.12);--df-red-ring: rgba(239,68,68,.3);--df-pink: #ec4899;--df-pink-text: #f472b6;--df-pink-soft: rgba(236,72,153,.12);--df-pink-ring: rgba(236,72,153,.3);--df-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--df-font-serif: "Source Serif 4", "Iowan Old Style", Georgia, serif;--df-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;--df-r-sm: 4px;--df-r-md: 6px;--df-r-lg: 8px;--df-r-xl: 10px;--df-r-pill: 999px;--df-h-btn: 28px;--df-h-input: 30px;--df-h-bar: 44px;--df-w-rail: 60px;--df-w-rail-detail: 280px;--df-w-tree: 260px;--df-ease: cubic-bezier(.2,.7,.3,1);--df-fast: .12s;--df-base: .2s;--df-slow: .35s;--df-content-max: 720px;--df-content-pad: 56px}*,*:before,*:after{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--df-bg);color:var(--df-text);font-family:var(--df-font-sans);font-feature-settings:"cv11","ss01";font-size:13px;line-height:1.5;letter-spacing:-.005em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button{font-family:inherit}.df-mono{font-family:var(--df-font-mono);font-feature-settings:"cv01","cv02"}.df-serif{font-family:var(--df-font-serif)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:var(--df-border-2);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--df-border-3)}::-webkit-scrollbar-track{background:transparent}::selection{background:var(--df-accent-soft);color:var(--df-text)}.df-app{width:100vw;height:100vh;display:flex;flex-direction:column;overflow:hidden}.df-app__main{flex:1;display:flex;overflow:hidden;min-height:0}.df-view{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:var(--df-bg)}@keyframes df-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes df-spin{to{transform:rotate(360deg)}}@keyframes df-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes df-blink{0%,49%{opacity:1}50%,to{opacity:0}}@keyframes df-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes df-bar-grow{0%{transform:scaleY(0)}to{transform:scaleY(1)}}@keyframes df-page-enter{0%{opacity:0;transform:translate(8px)}to{opacity:1;transform:translate(0)}}.df-anim-fade{animation:df-fade-in var(--df-base) var(--df-ease) both}.df-anim-page{animation:df-page-enter var(--df-slow) var(--df-ease) both}.df-pill{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:var(--df-r-sm);font-size:11px;font-weight:500;font-family:var(--df-font-sans);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text-3);white-space:nowrap}.df-pill--accent{background:var(--df-accent-soft);border-color:var(--df-accent-ring);color:var(--df-accent-text)}.df-pill--green{background:var(--df-green-soft);border-color:var(--df-green-ring);color:var(--df-green-text)}.df-pill--amber{background:var(--df-amber-soft);border-color:var(--df-amber-ring);color:var(--df-amber-text)}.df-pill--red{background:var(--df-red-soft);border-color:var(--df-red-ring);color:var(--df-red-text)}.df-pill--pink{background:var(--df-pink-soft);border-color:var(--df-pink-ring);color:var(--df-pink-text)}.df-pill__dot{width:5px;height:5px;border-radius:50%;background:currentColor}.df-btn{display:inline-flex;align-items:center;gap:6px;height:var(--df-h-btn);padding:0 10px;border-radius:var(--df-r-md);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text);font-size:12px;font-weight:500;letter-spacing:-.005em;cursor:pointer;transition:background var(--df-fast),border-color var(--df-fast),color var(--df-fast);white-space:nowrap}.df-btn:hover{background:var(--df-surface-2)}.df-btn--primary{background:var(--df-accent);border-color:var(--df-accent);color:#fff}.df-btn--primary:hover{background:var(--df-accent-hover);border-color:var(--df-accent-hover)}.df-btn--ghost{background:transparent;border-color:transparent;color:var(--df-text-3)}.df-btn--ghost:hover{background:var(--df-surface-3);color:var(--df-text)}.df-btn--active{background:var(--df-surface-2);border-color:var(--df-border-hi)}.df-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:var(--df-r-sm);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text-3);font:11px/1 var(--df-font-mono)}.df-btn--primary .df-kbd{background:#ffffff2e;border:none;color:#fff}.df-card{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-card--padded{padding:18px}.df-card--rows>*+*{border-top:1px solid var(--df-border)}.df-card__row{display:flex;align-items:center;gap:14px;padding:12px 16px;font-size:12px}.df-eyebrow{font-size:11px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em}.df-h1{margin:0;font-size:24px;font-weight:600;letter-spacing:-.02em;color:var(--df-text)}.df-h1--display{font-size:26px}.df-h1--serif{font-family:var(--df-font-serif);font-weight:400;font-size:32px;letter-spacing:-.02em;line-height:1.15}.df-subtle{color:var(--df-text-4);font-size:13px}.df-topbar{height:var(--df-h-bar);display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--df-bg);border-bottom:1px solid var(--df-border);flex-shrink:0;flex-wrap:nowrap;white-space:nowrap;overflow:hidden;min-width:0}.df-topbar>*{flex-shrink:0}.df-topbar__crumb{display:flex;align-items:center;gap:8px;font-size:13px}.df-topbar__sep{color:var(--df-border-3)}.df-topbar__meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--df-text-4);min-width:0;overflow:hidden;text-overflow:ellipsis}.df-topbar__meta>span:last-child{overflow:hidden;text-overflow:ellipsis}.df-topbar__divider{width:1px;height:18px;background:var(--df-border);margin:0 4px}.df-topbar__avatar{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--df-pink),var(--df-accent))}.df-rail{width:var(--df-w-rail);background:var(--df-bg);border-right:1px solid var(--df-border);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:2px;flex-shrink:0}.df-rail__logo{width:32px;height:32px;margin-bottom:14px;border-radius:var(--df-r-lg);background:linear-gradient(135deg,var(--df-accent),#a855f7);display:flex;align-items:center;justify-content:center;color:#fff}.df-rail__item{position:relative;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:var(--df-r-lg);color:var(--df-text-4);cursor:pointer;transition:background var(--df-fast),color var(--df-fast)}.df-rail__item:hover{color:var(--df-text-3)}.df-rail__item--active{background:var(--df-surface-3);color:var(--df-text)}.df-rail__item--active:before{content:"";position:absolute;left:-6px;top:10px;bottom:10px;width:2px;border-radius:1px;background:var(--df-accent)}.df-rail__spacer{flex:1}.df-rail__groups{width:100%;padding:0 8px}.df-rail__group{margin-bottom:8px}.df-rail__group-label{font-size:9px;letter-spacing:.12em;color:var(--df-text-5);text-transform:uppercase;text-align:center;margin-bottom:4px}.df-rail__divider{width:20px;height:1px;margin:4px auto;background:var(--df-border-3)}.df-rail__project-picker{width:32px;margin-bottom:4px}.df-rail__project-picker-wrap{position:relative;width:32px;height:32px;border-radius:var(--df-r-lg);background:var(--df-surface-3);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background var(--df-fast)}.df-rail__project-picker-wrap:hover,.df-rail__project-picker-wrap.open{background:var(--df-surface-2)}.df-rail__project-picker-caret{position:absolute;right:2px;top:50%;transform:translateY(-50%);width:0;height:0;border:3.5px solid var(--df-text-4);border-width:3.5px 0 3.5px 4px;transition:transform var(--df-fast)}.df-rail__project-picker-wrap.open .df-rail__project-picker-caret{transform:translateY(-50%) rotate(-90deg)}.df-rail__project-picker-menu{position:absolute;top:38px;left:0;width:220px;padding:4px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);box-shadow:var(--df-shadow);z-index:100;display:none}.df-rail__project-picker-wrap.open+.df-rail__project-picker-menu,.df-rail__project-picker-menu--open{display:block}.df-rail__project-picker-search{width:100%;padding:7px 10px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text);margin-bottom:4px}.df-rail__project-picker-list{max-height:180px;overflow-y:auto}.df-rail__project-picker-item{display:flex;align-items:center;gap:8px;padding:7px 8px;font-size:12px;color:var(--df-text-3);cursor:pointer;border-radius:var(--df-r-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.df-rail__project-picker-item:hover,.df-rail__project-picker-item--selected{background:var(--df-accent-halo);color:var(--df-accent-text)}.df-rail__project-picker-icon{width:16px;height:16px;flex-shrink:0}.df-rail__project-picker-item-badge{font-size:10px;padding:1px 6px;border-radius:var(--df-r-pill);background:var(--df-accent-halo);color:var(--df-accent-text);border:1px solid var(--df-accent-ring);white-space:nowrap;flex-shrink:0}.df-rail__project-picker-item-name{flex:1;overflow:hidden;text-overflow:ellipsis}.df-rail__project-picker-form{padding:4px;border-top:1px solid var(--df-border);margin-top:4px}.df-rail__project-picker-name{width:100%;padding:6px 8px;margin-bottom:4px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text);outline:none}.df-rail__project-picker-name:focus{border-color:var(--df-accent-ring);box-shadow:0 0 0 3px var(--df-accent-halo)}.df-rail__project-picker-select{width:100%;padding:6px 8px;margin-bottom:4px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text)}.df-rail__project-picker-create{width:100%;padding:5px;margin-top:4px;background:var(--df-accent);border-radius:var(--df-r-sm);font-size:11px;font-weight:600;color:#fff;cursor:pointer}.df-rail__project-picker-create:hover{opacity:.9}.df-rail__tip{pointer-events:none;position:absolute;left:46px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-sm);padding:3px 8px;font-size:11px;color:var(--df-text-2);white-space:nowrap;z-index:200;box-shadow:var(--df-shadow);transform:translate(-6px);opacity:0;transition:all var(--df-fast) var(--df-ease)}.df-rail__item:hover .df-rail__tip{transform:translate(0);opacity:1}.df-rail__item:hover .df-rail__tip--lg{left:auto;right:-70px}.df-rail--collapsed{width:64px!important}.df-rail--collapsed .df-rail__groups,.df-rail--collapsed .df-rail__project-picker{display:none}.df-rail--collapsed .df-rail__item:not(.df-rail__project-picker-wrap):hover .df-rail__tip{display:none}.df-field{display:block}.df-field__label{font-size:11px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}.df-field__value{padding:10px 12px;background:var(--df-surface);border:1px solid var(--df-border-2);border-radius:var(--df-r-lg);font-size:13px;color:var(--df-text)}.df-field__value--mono{font-family:var(--df-font-mono)}.df-code{background:var(--df-surface-3);border:1px solid var(--df-border-2);padding:1px 6px;border-radius:var(--df-r-sm);font-family:var(--df-font-mono);font-size:.82em;color:var(--df-accent-text)}.df-status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--df-text-4)}.df-status-dot--live{background:var(--df-green);animation:df-pulse 1.4s infinite}.df-status-dot--paused{background:var(--df-text-4)}.df-ask{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--df-bg)}.df-ask__header{padding:40px var(--df-content-pad) 24px;border-bottom:1px solid var(--df-border)}.df-ask__eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:10px}.df-ask__title-accent{color:var(--df-accent-text)}.df-ask__intro{margin:6px 0 0;color:var(--df-text-4);font-size:13px}.df-ask__box-wrap{margin-top:24px;position:relative}.df-ask__box{display:flex;align-items:center;gap:10px;padding:14px 18px;border-radius:var(--df-r-xl);background:var(--df-surface);border:1px solid var(--df-border-2);transition:all var(--df-base) var(--df-ease)}.df-ask__box--active{border-color:var(--df-accent-ring);box-shadow:0 0 0 4px var(--df-accent-halo)}.df-ask__box-input{flex:1;font-size:15px;color:var(--df-text);min-height:22px}.df-ask__placeholder{color:var(--df-text-4)}.df-ask__caret{display:inline-block;width:2px;height:16px;background:var(--df-accent-text);vertical-align:-2px;margin-left:1px;animation:df-blink .8s infinite}.df-ask__run{height:28px;padding:0 14px;border-radius:var(--df-r-md);background:var(--df-accent);color:#fff;border:none;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:5px}.df-ask__run:hover{background:var(--df-accent-hover)}.df-ask__reset{padding:4px 10px;border-radius:var(--df-r-md);background:transparent;color:var(--df-text-4);border:1px solid var(--df-border-3);font-size:11px;cursor:pointer}.df-ask__chips{margin-top:12px;display:flex;flex-wrap:wrap;gap:6px}.df-chip{padding:6px 11px;border-radius:var(--df-r-pill);background:transparent;border:1px solid var(--df-border-2);color:var(--df-text-3);font-size:12px;cursor:pointer;font-family:inherit;transition:all var(--df-fast)}.df-chip:hover{background:var(--df-surface-2);border-color:var(--df-border-hi);color:var(--df-text)}.df-ask__body{flex:1;display:flex;overflow:hidden;min-height:0}.df-ask__main{flex:1;padding:28px var(--df-content-pad) 56px;min-width:0;overflow:auto}.df-search__head{display:flex;align-items:center;gap:10px;color:var(--df-accent-text);font-size:13px;margin-bottom:18px}.df-search__spinner{width:14px;height:14px;border:2px solid var(--df-border-3);border-top-color:var(--df-accent);border-radius:50%;animation:df-spin .7s linear infinite}.df-search__stage{display:flex;align-items:center;gap:10px;padding:8px 0;font-size:12px;color:var(--df-text-4)}.df-search__stage--done{color:var(--df-green)}.df-search__skel{margin-top:24px;display:flex;flex-direction:column;gap:10px}.df-search__skel-bar{height:12px;border-radius:var(--df-r-sm);background:linear-gradient(90deg,var(--df-surface-2),var(--df-surface-3),var(--df-surface-2));background-size:200% 100%;animation:df-shimmer 1.4s infinite linear}.df-answer{max-width:var(--df-content-max)}.df-answer__meta{display:flex;align-items:center;gap:10px;margin-bottom:20px}.df-answer__doc{font-family:var(--df-font-serif);border-top:1px solid var(--df-border);border-bottom:1px solid var(--df-border);padding:28px 0 32px}.df-answer__h{margin:0 0 22px;font-size:24px;font-weight:400;color:var(--df-text);letter-spacing:-.01em;line-height:1.25}.df-answer__prose{font-size:16px;line-height:1.72;color:var(--df-text-2)}.df-answer__prose p{margin:0 0 16px}.df-answer__prose strong{color:var(--df-text);font-weight:600}.df-answer__sup{color:var(--df-accent-text);font-weight:600;margin-left:2px;font-family:var(--df-font-sans);font-size:.7em}.df-answer__list{margin:0 0 18px;padding-left:0;list-style:none}.df-answer__li{position:relative;padding:8px 0 8px 22px;margin-left:6px;border-left:1px solid var(--df-border-2)}.df-answer__num{position:absolute;left:-10px;top:11px;width:18px;height:18px;border-radius:50%;background:var(--df-bg);border:1px solid var(--df-border-hi);color:var(--df-accent-text);font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;font-family:var(--df-font-sans)}.df-cites{margin-top:24px;font-family:var(--df-font-serif)}.df-cites__row{display:flex;gap:14px;padding:12px 0;border-top:1px solid var(--df-border)}.df-cites__row:first-child{border-top:none}.df-cites__num{width:24px;color:var(--df-accent-text);font-weight:600;font-family:var(--df-font-sans);font-size:12px;margin-top:2px}.df-cites__title{font-size:15px;color:var(--df-text)}.df-cites__path{font-family:var(--df-font-mono);font-size:11px;color:var(--df-text-4);margin-top:2px}.df-related{margin-top:28px;padding-top:20px;border-top:1px solid var(--df-border)}.df-related__list{display:flex;flex-direction:column;gap:6px}.df-related__item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:transparent;border:1px solid var(--df-border);border-radius:var(--df-r-md);color:var(--df-text-3);font-size:13px;cursor:pointer;font-family:inherit;text-align:left;transition:border-color var(--df-fast),color var(--df-fast)}.df-related__item:hover{border-color:var(--df-border-3);color:var(--df-text)}.df-actions{margin-top:24px;display:flex;gap:8px}.df-cites-rail{width:300px;border-left:1px solid var(--df-border);padding:28px 20px;background:#0c0c0e;flex-shrink:0;overflow:auto}.df-cites-rail__list{display:flex;flex-direction:column;gap:8px}.df-cites-card{padding:12px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);cursor:pointer;transition:border-color var(--df-fast)}.df-cites-card:hover{border-color:var(--df-border-3)}.df-cites-card__head{display:flex;align-items:center;gap:6px;margin-bottom:6px}.df-cites-card__num{width:18px;height:18px;border-radius:5px;background:var(--df-accent-soft);color:var(--df-accent-text);font-size:10px;font-weight:600;display:flex;align-items:center;justify-content:center;font-family:var(--df-font-mono)}.df-cites-card__title{font-size:13px;font-weight:500;color:var(--df-text);flex:1}.df-cites-card__path{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono);margin-bottom:8px}.df-cites-card__foot{display:flex;align-items:center;gap:6px;font-size:10px}.df-activity__row{display:flex;align-items:center;gap:14px;padding:10px 14px;font-size:12px;border-top:1px solid var(--df-border)}.df-activity__row:first-child{border-top:none}.df-activity__time{width:48px;color:var(--df-text-4);font-size:11px;font-family:var(--df-font-mono)}.df-activity__target{flex:1;color:var(--df-text-3);font-family:var(--df-font-mono)}.df-activity__delta{color:var(--df-text-4)}.df-wiki{flex:1;display:flex;overflow:hidden}.df-tree{width:var(--df-w-tree);border-right:1px solid var(--df-border);padding:14px 10px;overflow:auto;flex-shrink:0}.df-tree__filter{display:flex;align-items:center;gap:6px;padding:6px 8px;margin-bottom:8px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-md);font-size:12px;color:var(--df-text-4)}.df-tree__heading{font-size:10px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em;padding:8px 8px 4px}.df-tree__row{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text-3);cursor:pointer;transition:background var(--df-fast)}.df-tree__row:hover{background:var(--df-surface-2)}.df-tree__row--active{background:var(--df-accent-soft);color:var(--df-accent-text)}.df-tree__row--active:hover{background:var(--df-accent-soft)}.df-tree__row--stale{color:var(--df-amber-text)}.df-tree__caret{width:11px;display:flex;color:var(--df-text-4)}.df-tree__indicator{width:5px;height:5px;border-radius:50%}.df-tree__indicator--stale{background:var(--df-amber)}.df-tree__indicator--highlight{background:var(--df-accent)}.df-page{flex:1;overflow:auto;padding:40px var(--df-content-pad);max-width:780px;font-family:var(--df-font-serif)}.df-page__crumb{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--df-text-4);margin-bottom:10px;font-family:var(--df-font-sans)}.df-page__crumb-active{color:var(--df-text-3)}.df-page__meta{display:flex;align-items:center;gap:8px;margin:14px 0 28px;font-family:var(--df-font-sans)}.df-page__path{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-page__prose{font-size:16px;line-height:1.72;color:var(--df-text-2)}.df-page__prose p:first-child{margin-top:0}.df-page__prose ul{padding-left:18px;line-height:1.7}.df-page__prose a{color:var(--df-accent-text);text-decoration:none}.df-page__prose a:hover{text-decoration:underline}.df-page__h3{font-family:var(--df-font-sans);font-size:13px;font-weight:600;color:var(--df-text);margin-top:28px;text-transform:uppercase;letter-spacing:.08em}.df-page__kv-row{display:flex;padding:8px 14px;border-top:1px solid var(--df-border);font-size:13px}.df-page__kv-row:first-child{border-top:none}.df-page__kv-key{width:120px;color:var(--df-accent-text);font-family:var(--df-font-mono)}.df-page__kv-val{color:var(--df-text-3)}.df-graph{flex:1;display:flex;overflow:hidden}.df-graph__canvas{flex:1;position:relative;overflow:hidden}.df-graph__grid{position:absolute;top:0;right:0;bottom:0;left:0;background-image:radial-gradient(circle,var(--df-border) 1px,transparent 1px);background-size:24px 24px;opacity:.5}.df-graph__legend{position:absolute;top:16px;left:16px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);padding:10px 12px;display:flex;flex-direction:column;gap:6px;font-size:11px;z-index:2}.df-graph__legend-row{display:flex;align-items:center;gap:7px;color:var(--df-text-3);text-transform:capitalize}.df-graph__legend-swatch{width:8px;height:8px;border-radius:50%}.df-graph__controls{position:absolute;top:16px;right:16px;display:flex;gap:6px;z-index:2}.df-graph__svg{position:absolute;top:0;right:0;bottom:0;left:0}.df-graph__detail{width:var(--df-w-rail-detail);border-left:1px solid var(--df-border);padding:24px 20px;flex-shrink:0;overflow:auto}.df-graph__connection{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:var(--df-r-sm);cursor:pointer;font-size:12px;color:var(--df-text-3)}.df-graph__connection:hover{background:var(--df-surface-2)}.df-graph__connection-dot{width:6px;height:6px;border-radius:50%}.df-health{flex:1;overflow:auto;padding:40px var(--df-content-pad)}.df-health__grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:24px 0 32px}.df-stat{padding:18px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-stat__num{font-size:34px;font-weight:600;letter-spacing:-.02em;margin-top:6px}.df-stat__num--green{color:var(--df-green)}.df-stat__num--amber{color:var(--df-amber-text)}.df-stat__num--red{color:var(--df-red)}.df-stat__bar{height:4px;background:var(--df-border);border-radius:2px;margin-top:10px;overflow:hidden}.df-stat__bar-fill{height:100%;background:linear-gradient(90deg,var(--df-green),var(--df-green-text));border-radius:2px}.df-stat__caption{font-size:11px;color:var(--df-text-4);margin-top:10px}.df-trend{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:20px;margin-bottom:24px}.df-trend__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.df-trend__title{font-size:13px;font-weight:500}.df-trend__bars{display:flex;align-items:flex-end;gap:6px;height:80px}.df-trend__bar{flex:1;border-radius:3px 3px 0 0;opacity:.85;transform-origin:bottom;animation:df-bar-grow .5s var(--df-ease) both}.df-issues__row{display:flex;align-items:center;gap:14px;padding:14px 16px;border-top:1px solid var(--df-border)}.df-issues__row:first-child{border-top:none}.df-issues__page{font-size:13px;font-weight:500;color:var(--df-text)}.df-issues__msg{flex:1;font-size:12px;color:var(--df-text-3)}.df-issues__age{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-sync{flex:1;overflow:auto;padding:40px var(--df-content-pad)}.df-sync__head{display:flex;align-items:center;justify-content:space-between}.df-sync__cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:24px 0 28px}.df-sync-card{padding:16px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-sync-card__body{margin-top:8px;font-weight:500;display:flex;align-items:center;gap:8px}.df-sync-card__sub{font-size:11px;color:var(--df-text-4);margin-top:6px;font-family:var(--df-font-mono)}.df-sync-card__sub--green{color:var(--df-green);font-family:inherit}.df-timeline{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:8px 0}.df-timeline__row{display:flex;align-items:center;gap:14px;padding:10px 18px;position:relative}.df-timeline__time{width:48px;font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-timeline__line{position:absolute;left:78px;top:0;bottom:0;width:1px;background:var(--df-border)}.df-timeline__dot{position:relative;width:9px;height:9px;border-radius:50%;background:var(--df-border-3);z-index:1}.df-timeline__dot--commit{background:var(--df-pink)}.df-timeline__dot--sync{background:var(--df-accent)}.df-timeline__dot--done{background:var(--df-green);box-shadow:0 0 0 3px #10b9812e}.df-timeline__msg{flex:1;font-size:13px;color:var(--df-text-2)}.df-timeline__msg--done{color:var(--df-green)}.df-onboard{flex:1;display:flex;flex-direction:column;overflow:auto}.df-onboard__center{flex:1;display:flex;align-items:center;justify-content:center;padding:48px 32px}.df-onboard__inner{width:100%;max-width:640px}.df-onboard__steps{display:flex;align-items:center;gap:8px;margin-bottom:32px;font-size:12px}.df-onboard__step{display:flex;align-items:center;gap:8px;color:var(--df-text-4)}.df-onboard__step--active,.df-onboard__step--done{color:var(--df-text)}.df-onboard__step-num{width:22px;height:22px;border-radius:50%;background:var(--df-surface-3);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600}.df-onboard__step--active .df-onboard__step-num{background:var(--df-accent)}.df-onboard__step--done .df-onboard__step-num{background:var(--df-green)}.df-onboard__step-line{flex:1;height:1px;background:var(--df-border)}.df-onboard__step-line--done{background:var(--df-green)}.df-onboard__intro{color:var(--df-text-4);margin:6px 0 24px}.df-domains{display:grid;grid-template-columns:1fr 1fr;gap:10px}.df-domain{text-align:left;padding:18px;border-radius:var(--df-r-xl);background:var(--df-surface);border:1px solid var(--df-border);cursor:pointer;font-family:inherit;color:var(--df-text);transition:all var(--df-fast)}.df-domain--selected{background:var(--df-accent-halo);border-color:var(--df-accent)}.df-domain__icon{width:32px;height:32px;border-radius:var(--df-r-lg);background:var(--df-surface-3);display:flex;align-items:center;justify-content:center;margin-bottom:10px;color:var(--df-text-3)}.df-domain--selected .df-domain__icon{background:var(--df-accent-soft);color:var(--df-accent-text)}.df-domain__label{font-weight:500;font-size:14px}.df-domain__desc{font-size:12px;color:var(--df-text-4);margin-top:4px}.df-onboard__fields{display:flex;flex-direction:column;gap:14px}.df-dropzone{padding:32px;border:1.5px dashed var(--df-border-3);border-radius:12px;text-align:center;background:#0c0c0e}.df-dropzone__title{margin-top:12px;font-size:14px;color:var(--df-text)}.df-dropzone__sub{margin-top:4px;font-size:12px;color:var(--df-text-4)}.df-init-log{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:16px;font-family:var(--df-font-mono);font-size:12px}.df-init-log__line{display:flex;gap:10px;padding:4px 0;color:var(--df-text-3)}.df-init-log__mark--ok{color:var(--df-green)}.df-init-log__mark--run{color:var(--df-accent-text)}.df-onboard__nav{display:flex;justify-content:space-between;margin-top:32px}
|