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.
- package/dist/commands/config.js +102 -0
- package/dist/commands/down.js +26 -0
- package/dist/commands/groups.js +43 -0
- package/dist/commands/ls.js +23 -0
- package/dist/commands/up.js +39 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.js +0 -0
- package/dist/index.js +35 -35
- package/dist/process/manager.js +226 -0
- package/dist/process/pid-store.js +141 -0
- package/dist/process/template.js +53 -0
- package/package.json +2 -1
- package/src/commands/down.ts +28 -3
- package/src/commands/groups.ts +1 -1
- package/src/commands/up.ts +5 -0
- package/src/process/manager.ts +102 -2
- package/src/process/pid-store.ts +192 -0
- package/tests/integration/commands.test.ts +3 -3
|
@@ -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('
|
|
308
|
-
assert.ok(output.includes('
|
|
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 () => {
|