cligr 1.0.0 → 1.0.2

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,192 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export interface PidEntry {
6
+ pid: number;
7
+ groupName: string;
8
+ itemName: string;
9
+ startTime: number;
10
+ restartPolicy: 'yes' | 'no' | 'unless-stopped';
11
+ fullCmd: string;
12
+ }
13
+
14
+ export class PidStore {
15
+ private readonly pidsDir: string;
16
+
17
+ constructor() {
18
+ this.pidsDir = path.join(os.homedir(), '.cligr', 'pids');
19
+ }
20
+
21
+ /**
22
+ * Initialize the PID directory
23
+ */
24
+ async ensureDir(): Promise<void> {
25
+ try {
26
+ await fs.mkdir(this.pidsDir, { recursive: true });
27
+ } catch (err) {
28
+ // Ignore if already exists
29
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
30
+ throw err;
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get the PID file path for a specific group and item
37
+ */
38
+ private getPidFilePath(groupName: string, itemName: string): string {
39
+ return path.join(this.pidsDir, `${groupName}_${itemName}.pid`);
40
+ }
41
+
42
+ /**
43
+ * Write a PID file with metadata
44
+ */
45
+ async writePid(entry: PidEntry): Promise<void> {
46
+ await this.ensureDir();
47
+ const filePath = this.getPidFilePath(entry.groupName, entry.itemName);
48
+ await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8');
49
+ }
50
+
51
+ /**
52
+ * Read all PID entries for a specific group
53
+ */
54
+ async readPidsByGroup(groupName: string): Promise<PidEntry[]> {
55
+ await this.ensureDir();
56
+ const entries: PidEntry[] = [];
57
+
58
+ try {
59
+ const files = await fs.readdir(this.pidsDir);
60
+ const prefix = `${groupName}_`;
61
+
62
+ for (const file of files) {
63
+ if (file.startsWith(prefix) && file.endsWith('.pid')) {
64
+ try {
65
+ const content = await fs.readFile(path.join(this.pidsDir, file), 'utf-8');
66
+ entries.push(JSON.parse(content) as PidEntry);
67
+ } catch {
68
+ // Skip invalid files
69
+ continue;
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ // Directory doesn't exist or can't be read
75
+ return [];
76
+ }
77
+
78
+ return entries;
79
+ }
80
+
81
+ /**
82
+ * Read all PID entries
83
+ */
84
+ async readAllPids(): Promise<PidEntry[]> {
85
+ await this.ensureDir();
86
+ const entries: PidEntry[] = [];
87
+
88
+ try {
89
+ const files = await fs.readdir(this.pidsDir);
90
+
91
+ for (const file of files) {
92
+ if (file.endsWith('.pid')) {
93
+ try {
94
+ const content = await fs.readFile(path.join(this.pidsDir, file), 'utf-8');
95
+ entries.push(JSON.parse(content) as PidEntry);
96
+ } catch {
97
+ // Skip invalid files
98
+ continue;
99
+ }
100
+ }
101
+ }
102
+ } catch {
103
+ // Directory doesn't exist or can't be read
104
+ return [];
105
+ }
106
+
107
+ return entries;
108
+ }
109
+
110
+ /**
111
+ * Delete a specific PID file
112
+ */
113
+ async deletePid(groupName: string, itemName: string): Promise<void> {
114
+ const filePath = this.getPidFilePath(groupName, itemName);
115
+ try {
116
+ await fs.unlink(filePath);
117
+ } catch (err) {
118
+ // Ignore if file doesn't exist
119
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
120
+ throw err;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Delete all PID files for a group
127
+ */
128
+ async deleteGroupPids(groupName: string): Promise<void> {
129
+ const entries = await this.readPidsByGroup(groupName);
130
+ for (const entry of entries) {
131
+ await this.deletePid(entry.groupName, entry.itemName);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Check if a PID is currently running (cross-platform)
137
+ */
138
+ isPidRunning(pid: number): boolean {
139
+ try {
140
+ // Sending signal 0 checks if process exists without killing it
141
+ process.kill(pid, 0);
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Check if a PID entry is still valid and running.
150
+ * This helps prevent killing wrong processes if PID is reused by the OS.
151
+ * A PID is considered valid if it's running AND started recently (within last 5 minutes).
152
+ */
153
+ isPidEntryValid(entry: PidEntry): boolean {
154
+ if (!this.isPidRunning(entry.pid)) {
155
+ return false;
156
+ }
157
+
158
+ // Check if the process start time is recent (within 5 minutes)
159
+ // This prevents killing a wrong process if PID was reused
160
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
161
+ return entry.startTime > fiveMinutesAgo;
162
+ }
163
+
164
+ /**
165
+ * Remove PID files for processes that are no longer running
166
+ * Returns the list of stale entries that were removed
167
+ */
168
+ async cleanupStalePids(): Promise<PidEntry[]> {
169
+ const allEntries = await this.readAllPids();
170
+ const staleEntries: PidEntry[] = [];
171
+
172
+ for (const entry of allEntries) {
173
+ // Check if PID is no longer running OR if entry is too old (> 5 minutes)
174
+ // This helps prevent PID reuse issues
175
+ if (!this.isPidEntryValid(entry)) {
176
+ staleEntries.push(entry);
177
+ await this.deletePid(entry.groupName, entry.itemName);
178
+ }
179
+ }
180
+
181
+ return staleEntries;
182
+ }
183
+
184
+ /**
185
+ * Get all unique group names that have PID files
186
+ */
187
+ async getRunningGroups(): Promise<string[]> {
188
+ const allEntries = await this.readAllPids();
189
+ const groups = new Set(allEntries.map(e => e.groupName));
190
+ return Array.from(groups);
191
+ }
192
+ }
@@ -297,15 +297,15 @@ groups:
297
297
  });
298
298
 
299
299
  describe('downCommand', () => {
300
- it('should return success and display message', async () => {
300
+ it('should return success and display message when group is not running', async () => {
301
301
  resetOutput();
302
302
 
303
303
  const exitCode = await downCommand('test-group');
304
304
 
305
305
  assert.strictEqual(exitCode, 0);
306
306
  const output = getLogOutput();
307
- assert.ok(output.includes('down test-group'));
308
- assert.ok(output.includes('will stop'));
307
+ assert.ok(output.includes('test-group'));
308
+ assert.ok(output.includes('not running'));
309
309
  });
310
310
 
311
311
  it('should work with any group name', async () => {