@masslessai/push-todo 3.0.0 → 3.1.0

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,193 @@
1
+ /**
2
+ * Daemon health management for Push CLI.
3
+ *
4
+ * Auto-starts daemon on any /push-todo command if not running.
5
+ * This is the "self-healing" behavior - same as Python version.
6
+ *
7
+ * Ported from: plugins/push-todo/scripts/daemon_health.py
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
11
+ import { spawn } from 'child_process';
12
+ import { homedir } from 'os';
13
+ import { join, dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ const PUSH_DIR = join(homedir(), '.push');
20
+ const PID_FILE = join(PUSH_DIR, 'daemon.pid');
21
+ const LOG_FILE = join(PUSH_DIR, 'daemon.log');
22
+ const STATUS_FILE = join(PUSH_DIR, 'daemon_status.json');
23
+ const VERSION_FILE = join(PUSH_DIR, 'daemon.version');
24
+
25
+ /**
26
+ * Check if a process is running by PID.
27
+ */
28
+ function isProcessRunning(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get the current daemon status.
39
+ * Same as Python's get_daemon_status().
40
+ */
41
+ export function getDaemonStatus() {
42
+ const status = {
43
+ running: false,
44
+ pid: null,
45
+ uptime: null,
46
+ version: null,
47
+ log_file: LOG_FILE
48
+ };
49
+
50
+ if (!existsSync(PID_FILE)) {
51
+ return status;
52
+ }
53
+
54
+ try {
55
+ const pidStr = readFileSync(PID_FILE, 'utf8').trim();
56
+ const pid = parseInt(pidStr, 10);
57
+
58
+ if (isNaN(pid)) {
59
+ return status;
60
+ }
61
+
62
+ if (isProcessRunning(pid)) {
63
+ status.running = true;
64
+ status.pid = pid;
65
+
66
+ // Get version from version file
67
+ if (existsSync(VERSION_FILE)) {
68
+ try {
69
+ status.version = readFileSync(VERSION_FILE, 'utf8').trim();
70
+ } catch {}
71
+ }
72
+
73
+ // Get uptime and details from status file
74
+ if (existsSync(STATUS_FILE)) {
75
+ try {
76
+ const data = JSON.parse(readFileSync(STATUS_FILE, 'utf8'));
77
+ if (data.startedAt) {
78
+ status.uptime = formatUptime(data.startedAt);
79
+ }
80
+ status.runningTasks = data.runningTasks || [];
81
+ status.queuedTasks = data.queuedTasks || [];
82
+ status.completedToday = data.completedToday || [];
83
+ } catch {}
84
+ }
85
+ } else {
86
+ // Stale PID file
87
+ try { unlinkSync(PID_FILE); } catch {}
88
+ }
89
+ } catch {}
90
+
91
+ return status;
92
+ }
93
+
94
+ /**
95
+ * Format uptime for display.
96
+ */
97
+ function formatUptime(startedAt) {
98
+ if (!startedAt) return 'unknown';
99
+
100
+ try {
101
+ const started = new Date(startedAt);
102
+ const now = new Date();
103
+ const diff = Math.floor((now - started) / 1000);
104
+
105
+ const days = Math.floor(diff / 86400);
106
+ const hours = Math.floor((diff % 86400) / 3600);
107
+ const minutes = Math.floor((diff % 3600) / 60);
108
+ const seconds = diff % 60;
109
+
110
+ if (days > 0) return `${days}d ${hours}h`;
111
+ if (hours > 0) return `${hours}h ${minutes}m`;
112
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
113
+ return `${seconds}s`;
114
+ } catch {
115
+ return 'unknown';
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Start the daemon process.
121
+ * Same as Python's start_daemon().
122
+ */
123
+ export function startDaemon() {
124
+ const status = getDaemonStatus();
125
+ if (status.running) {
126
+ return true;
127
+ }
128
+
129
+ mkdirSync(PUSH_DIR, { recursive: true });
130
+
131
+ const daemonScript = join(__dirname, 'daemon.js');
132
+
133
+ if (!existsSync(daemonScript)) {
134
+ return false;
135
+ }
136
+
137
+ try {
138
+ const child = spawn(process.execPath, [daemonScript], {
139
+ detached: true,
140
+ stdio: ['ignore', 'ignore', 'ignore'],
141
+ env: { ...process.env, PUSH_DAEMON: '1' }
142
+ });
143
+
144
+ writeFileSync(PID_FILE, String(child.pid));
145
+ child.unref();
146
+
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Stop the daemon process.
155
+ */
156
+ export function stopDaemon() {
157
+ const status = getDaemonStatus();
158
+ if (!status.running) return true;
159
+
160
+ try {
161
+ process.kill(status.pid, 'SIGTERM');
162
+
163
+ // Wait for graceful shutdown
164
+ let waited = 0;
165
+ while (waited < 5000 && isProcessRunning(status.pid)) {
166
+ const start = Date.now();
167
+ while (Date.now() - start < 100) {} // Busy wait 100ms
168
+ waited += 100;
169
+ }
170
+
171
+ if (isProcessRunning(status.pid)) {
172
+ process.kill(status.pid, 'SIGKILL');
173
+ }
174
+
175
+ if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
176
+ return true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Ensure daemon is running - called on every /push-todo command.
184
+ * Same as Python's ensure_daemon_running().
185
+ */
186
+ export function ensureDaemonRunning() {
187
+ const status = getDaemonStatus();
188
+ if (!status.running) {
189
+ startDaemon();
190
+ }
191
+ }
192
+
193
+ export { PID_FILE, LOG_FILE, STATUS_FILE, PUSH_DIR };