@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.
@@ -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
+ }