@doquflow/cli 0.4.5 → 0.5.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/init.js +174 -3
- package/dist/commands/sync.js +361 -0
- package/dist/commands/watch-stop.js +223 -0
- package/dist/commands/watch.js +587 -0
- package/dist/index.js +92 -8
- package/package.json +2 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docuflow watch stop — stop the running watch daemon for this project
|
|
4
|
+
* docuflow watch status — show whether the daemon is running + details
|
|
5
|
+
* docuflow watch restart — stop the current daemon then start a new one
|
|
6
|
+
*
|
|
7
|
+
* All three commands use the PID file at .docuflow/watch.pid written when
|
|
8
|
+
* the daemon starts. The PID file contains: pid, started_at, bridge, options.
|
|
9
|
+
*
|
|
10
|
+
* stop → SIGTERM → wait up to 5s → SIGKILL if still alive → remove PID file
|
|
11
|
+
* status → check PID alive, show uptime/bridge/project
|
|
12
|
+
* restart → stop + re-spawn `docuflow watch` with same options
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.runStop = runStop;
|
|
19
|
+
exports.runStatus = runStatus;
|
|
20
|
+
exports.runRestart = runRestart;
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const node_child_process_1 = require("node:child_process");
|
|
23
|
+
const watch_1 = require("./watch");
|
|
24
|
+
// ─── Colour helpers ─────────────────────────────────────────────────────────
|
|
25
|
+
const c = {
|
|
26
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
28
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
29
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
30
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
32
|
+
};
|
|
33
|
+
// ─── Uptime formatter ────────────────────────────────────────────────────────
|
|
34
|
+
function formatUptime(startedAt) {
|
|
35
|
+
const ms = Date.now() - new Date(startedAt).getTime();
|
|
36
|
+
const secs = Math.floor(ms / 1000);
|
|
37
|
+
const mins = Math.floor(secs / 60);
|
|
38
|
+
const hours = Math.floor(mins / 60);
|
|
39
|
+
const days = Math.floor(hours / 24);
|
|
40
|
+
if (days > 0)
|
|
41
|
+
return `${days}d ${hours % 24}h`;
|
|
42
|
+
if (hours > 0)
|
|
43
|
+
return `${hours}h ${mins % 60}m`;
|
|
44
|
+
if (mins > 0)
|
|
45
|
+
return `${mins}m ${secs % 60}s`;
|
|
46
|
+
return `${secs}s`;
|
|
47
|
+
}
|
|
48
|
+
// ─── stop ────────────────────────────────────────────────────────────────────
|
|
49
|
+
async function runStop(projectPath) {
|
|
50
|
+
projectPath = node_path_1.default.resolve(projectPath);
|
|
51
|
+
const data = await (0, watch_1.readPidFile)(projectPath);
|
|
52
|
+
if (!data) {
|
|
53
|
+
console.log(c.yellow(" ⚠ No watch.pid file found — daemon may not be running."));
|
|
54
|
+
console.log(c.dim(` (looked in ${(0, watch_1.getPidFilePath)(projectPath)})`));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
if (!(0, watch_1.isProcessAlive)(data.pid)) {
|
|
58
|
+
console.log(c.yellow(` ⚠ PID ${data.pid} is no longer alive (stale PID file).`));
|
|
59
|
+
await (0, watch_1.removePidFile)(projectPath);
|
|
60
|
+
console.log(c.dim(" Stale PID file removed."));
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
console.log(`\n 🛑 Stopping DocuFlow watch daemon`);
|
|
64
|
+
console.log(` PID: ${data.pid}`);
|
|
65
|
+
console.log(` Bridge: ${data.bridge}`);
|
|
66
|
+
console.log(` Uptime: ${formatUptime(data.started_at)}`);
|
|
67
|
+
console.log();
|
|
68
|
+
// Send SIGTERM — gives daemon chance to clean up gracefully
|
|
69
|
+
try {
|
|
70
|
+
process.kill(data.pid, "SIGTERM");
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(c.red(` ✗ Failed to send SIGTERM: ${e.message}`));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
// Wait up to 5s for the process to exit, then SIGKILL
|
|
77
|
+
const deadline = Date.now() + 5000;
|
|
78
|
+
while ((0, watch_1.isProcessAlive)(data.pid)) {
|
|
79
|
+
if (Date.now() > deadline) {
|
|
80
|
+
console.log(c.yellow(" ⚠ Still alive after 5s — sending SIGKILL..."));
|
|
81
|
+
try {
|
|
82
|
+
process.kill(data.pid, "SIGKILL");
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
await new Promise(r => setTimeout(r, 500));
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
await new Promise(r => setTimeout(r, 200));
|
|
89
|
+
}
|
|
90
|
+
// Clean up PID file if daemon didn't remove it itself
|
|
91
|
+
await (0, watch_1.removePidFile)(projectPath);
|
|
92
|
+
if (!(0, watch_1.isProcessAlive)(data.pid)) {
|
|
93
|
+
console.log(c.green(" ✅ Watch daemon stopped successfully."));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(c.yellow(" ⚠ Process may still be running — check manually."));
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
// ─── status ──────────────────────────────────────────────────────────────────
|
|
101
|
+
async function runStatus(projectPath) {
|
|
102
|
+
projectPath = node_path_1.default.resolve(projectPath);
|
|
103
|
+
const data = await (0, watch_1.readPidFile)(projectPath);
|
|
104
|
+
console.log(`\n 📡 DocuFlow Watch Status`);
|
|
105
|
+
console.log(" ─────────────────────────────────────────────────");
|
|
106
|
+
if (!data) {
|
|
107
|
+
console.log(` State: ${c.dim("stopped")} (no watch.pid file)`);
|
|
108
|
+
console.log(` Project: ${projectPath}`);
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(c.dim(` Run "docuflow watch" to start the daemon.`));
|
|
111
|
+
console.log();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
const alive = (0, watch_1.isProcessAlive)(data.pid);
|
|
115
|
+
if (!alive) {
|
|
116
|
+
console.log(` State: ${c.yellow("stopped")} (stale PID file — process died unexpectedly)`);
|
|
117
|
+
console.log(` Last PID: ${data.pid}`);
|
|
118
|
+
console.log(` Started: ${new Date(data.started_at).toLocaleString()}`);
|
|
119
|
+
console.log(` Bridge: ${data.bridge}`);
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(c.dim(` Run "docuflow watch" to restart.`));
|
|
122
|
+
// Clean up stale PID file
|
|
123
|
+
await (0, watch_1.removePidFile)(projectPath);
|
|
124
|
+
console.log(c.dim(` (Stale PID file cleaned up)`));
|
|
125
|
+
console.log();
|
|
126
|
+
process.exit(1); // non-zero: daemon is not healthy
|
|
127
|
+
}
|
|
128
|
+
const bridgeIcon = data.bridge === "copilot" || data.bridge === "claude" ? "⚡" :
|
|
129
|
+
data.bridge === "codex" || data.bridge === "api" ? "🔤" : "📁";
|
|
130
|
+
const bridgeLabel = data.bridge === "none" ? c.dim("sources-only (no AI)") :
|
|
131
|
+
data.bridge === "copilot" ? c.green("copilot — direct MCP ⚡") :
|
|
132
|
+
data.bridge === "claude" ? c.green("claude — direct MCP ⚡") :
|
|
133
|
+
data.bridge === "codex" ? c.yellow("codex — doc-gen mode") :
|
|
134
|
+
data.bridge === "api" ? c.yellow("api — doc-gen mode") : data.bridge;
|
|
135
|
+
console.log(` State: ${c.green("● running")}`);
|
|
136
|
+
console.log(` PID: ${data.pid}`);
|
|
137
|
+
console.log(` Uptime: ${formatUptime(data.started_at)}`);
|
|
138
|
+
console.log(` Started: ${new Date(data.started_at).toLocaleString()}`);
|
|
139
|
+
console.log(` Bridge: ${bridgeLabel}`);
|
|
140
|
+
console.log(` Project: ${data.project_path}`);
|
|
141
|
+
if (data.options.lintIntervalHours) {
|
|
142
|
+
console.log(` Lint: every ${data.options.lintIntervalHours}h`);
|
|
143
|
+
}
|
|
144
|
+
if (data.options.codeExtensions?.length) {
|
|
145
|
+
console.log(` Exts: ${data.options.codeExtensions.join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(c.dim(` Run "docuflow watch stop" to stop the daemon.`));
|
|
149
|
+
console.log(c.dim(` Run "docuflow watch restart" to restart with same options.`));
|
|
150
|
+
console.log();
|
|
151
|
+
}
|
|
152
|
+
// ─── restart ─────────────────────────────────────────────────────────────────
|
|
153
|
+
async function runRestart(projectPath) {
|
|
154
|
+
projectPath = node_path_1.default.resolve(projectPath);
|
|
155
|
+
const data = await (0, watch_1.readPidFile)(projectPath);
|
|
156
|
+
console.log(`\n 🔄 Restarting DocuFlow watch daemon\n`);
|
|
157
|
+
// Capture current options before stopping
|
|
158
|
+
const opts = data?.options ?? {
|
|
159
|
+
ai: false,
|
|
160
|
+
forceCopilot: false,
|
|
161
|
+
forceClaude: false,
|
|
162
|
+
forceCodex: false,
|
|
163
|
+
lintIntervalHours: 24,
|
|
164
|
+
};
|
|
165
|
+
// Stop existing daemon if running
|
|
166
|
+
if (data && (0, watch_1.isProcessAlive)(data.pid)) {
|
|
167
|
+
console.log(` Stopping PID ${data.pid} (bridge: ${data.bridge}, uptime: ${formatUptime(data.started_at)})...`);
|
|
168
|
+
try {
|
|
169
|
+
process.kill(data.pid, "SIGTERM");
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
const deadline = Date.now() + 5000;
|
|
173
|
+
while ((0, watch_1.isProcessAlive)(data.pid) && Date.now() < deadline) {
|
|
174
|
+
await new Promise(r => setTimeout(r, 200));
|
|
175
|
+
}
|
|
176
|
+
if ((0, watch_1.isProcessAlive)(data.pid)) {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(data.pid, "SIGKILL");
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
}
|
|
182
|
+
await (0, watch_1.removePidFile)(projectPath);
|
|
183
|
+
console.log(c.green(" ✅ Previous daemon stopped."));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(c.dim(" No running daemon found — starting fresh."));
|
|
187
|
+
if (data)
|
|
188
|
+
await (0, watch_1.removePidFile)(projectPath);
|
|
189
|
+
}
|
|
190
|
+
// Build args for new daemon
|
|
191
|
+
const cliArgs = ["watch"];
|
|
192
|
+
if (opts.ai)
|
|
193
|
+
cliArgs.push("--ai");
|
|
194
|
+
if (opts.forceCopilot)
|
|
195
|
+
cliArgs.push("--copilot");
|
|
196
|
+
if (opts.forceClaude)
|
|
197
|
+
cliArgs.push("--claude");
|
|
198
|
+
if (opts.forceCodex)
|
|
199
|
+
cliArgs.push("--codex");
|
|
200
|
+
if (opts.lintIntervalHours !== 24) {
|
|
201
|
+
cliArgs.push("--lint-interval", String(opts.lintIntervalHours));
|
|
202
|
+
}
|
|
203
|
+
if (opts.codeExtensions?.length) {
|
|
204
|
+
cliArgs.push("--code-ext", opts.codeExtensions.join(","));
|
|
205
|
+
}
|
|
206
|
+
// Resolve CLI entry point
|
|
207
|
+
const cliBin = require.resolve("./index")
|
|
208
|
+
.replace(/\.ts$/, ".js")
|
|
209
|
+
.replace("/src/", "/dist/");
|
|
210
|
+
console.log(`\n Spawning: node ${node_path_1.default.basename(cliBin)} ${cliArgs.join(" ")}`);
|
|
211
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [cliBin, ...cliArgs], {
|
|
212
|
+
cwd: projectPath,
|
|
213
|
+
detached: true,
|
|
214
|
+
stdio: "inherit",
|
|
215
|
+
});
|
|
216
|
+
child.unref(); // let parent exit, child runs independently
|
|
217
|
+
console.log(c.green(`\n ✅ New watch daemon spawned (PID will appear above).`));
|
|
218
|
+
console.log(c.dim(` Run "docuflow watch status" to confirm.`));
|
|
219
|
+
console.log();
|
|
220
|
+
// Give it a moment to write the PID file, then show status
|
|
221
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
222
|
+
await runStatus(projectPath);
|
|
223
|
+
}
|