jm2 0.1.4 → 0.1.7
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/package.json +1 -1
- package/src/cli/commands/edit.js +34 -4
- package/src/cli/commands/tags.js +399 -0
- package/src/cli/index.js +28 -0
- package/src/daemon/index.js +247 -0
- package/src/daemon/scheduler.js +26 -1
- package/src/ipc/protocol.js +83 -0
- package/src/ipc/server.js +36 -4
- package/src/utils/paths.js +3 -7
package/package.json
CHANGED
package/src/cli/commands/edit.js
CHANGED
|
@@ -42,7 +42,19 @@ export async function editCommand(jobRef, options = {}) {
|
|
|
42
42
|
options.env !== undefined ||
|
|
43
43
|
options.timeout !== undefined ||
|
|
44
44
|
options.retry !== undefined ||
|
|
45
|
-
options.tag !== undefined
|
|
45
|
+
options.tag !== undefined ||
|
|
46
|
+
options.tagAppend !== undefined ||
|
|
47
|
+
options.tagRemove !== undefined;
|
|
48
|
+
|
|
49
|
+
// Check for mutually exclusive tag options
|
|
50
|
+
const hasTagSet = options.tag !== undefined;
|
|
51
|
+
const hasTagAppend = options.tagAppend !== undefined;
|
|
52
|
+
const hasTagRemove = options.tagRemove !== undefined;
|
|
53
|
+
|
|
54
|
+
if (hasTagSet && (hasTagAppend || hasTagRemove)) {
|
|
55
|
+
printError('Cannot use --tag with --tag-append or --tag-remove. Use --tag to replace all tags, or --tag-append/--tag-remove to modify existing tags.');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
46
58
|
|
|
47
59
|
if (!hasUpdates) {
|
|
48
60
|
printError('No changes specified. Use options like --command, --cron, --name, etc.');
|
|
@@ -143,12 +155,30 @@ export async function editCommand(jobRef, options = {}) {
|
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
if (options.tag !== undefined) {
|
|
146
|
-
// Handle multiple tags
|
|
158
|
+
// Handle multiple tags (replace mode)
|
|
147
159
|
const tags = Array.isArray(options.tag)
|
|
148
160
|
? options.tag
|
|
149
161
|
: options.tag ? [options.tag] : [];
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
updates.tags = tags;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.tagAppend !== undefined) {
|
|
166
|
+
// Handle tags to append
|
|
167
|
+
const tagsToAppend = Array.isArray(options.tagAppend)
|
|
168
|
+
? options.tagAppend
|
|
169
|
+
: options.tagAppend ? [options.tagAppend] : [];
|
|
170
|
+
if (tagsToAppend.length > 0) {
|
|
171
|
+
updates.tagsAppend = tagsToAppend;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.tagRemove !== undefined) {
|
|
176
|
+
// Handle tags to remove
|
|
177
|
+
const tagsToRemove = Array.isArray(options.tagRemove)
|
|
178
|
+
? options.tagRemove
|
|
179
|
+
: options.tagRemove ? [options.tagRemove] : [];
|
|
180
|
+
if (tagsToRemove.length > 0) {
|
|
181
|
+
updates.tagsRemove = tagsToRemove;
|
|
152
182
|
}
|
|
153
183
|
}
|
|
154
184
|
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 tags command
|
|
3
|
+
* Manage job tags - list, add, remove, rename, and more
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { send } from '../../ipc/client.js';
|
|
7
|
+
import { MessageType } from '../../ipc/protocol.js';
|
|
8
|
+
import {
|
|
9
|
+
printSuccess,
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printHeader,
|
|
13
|
+
createJobTable,
|
|
14
|
+
colorizeStatus,
|
|
15
|
+
formatRelativeTime,
|
|
16
|
+
} from '../utils/output.js';
|
|
17
|
+
import { isDaemonRunning } from '../../daemon/index.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute the tags command
|
|
22
|
+
* @param {string} subcommand - Subcommand to run (list, add, rm, clear, rename, jobs)
|
|
23
|
+
* @param {string[]} args - Arguments for the subcommand
|
|
24
|
+
* @param {object} options - Command options
|
|
25
|
+
* @returns {Promise<number>} Exit code
|
|
26
|
+
*/
|
|
27
|
+
export async function tagsCommand(subcommand, args, options = {}) {
|
|
28
|
+
// Check if daemon is running
|
|
29
|
+
if (!isDaemonRunning()) {
|
|
30
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
switch (subcommand) {
|
|
35
|
+
case 'list':
|
|
36
|
+
return await listTags(options);
|
|
37
|
+
case 'add':
|
|
38
|
+
return await addTags(args, options);
|
|
39
|
+
case 'rm':
|
|
40
|
+
case 'remove':
|
|
41
|
+
return await removeTags(args, options);
|
|
42
|
+
case 'clear':
|
|
43
|
+
return await clearTags(args, options);
|
|
44
|
+
case 'rename':
|
|
45
|
+
return await renameTag(args, options);
|
|
46
|
+
case 'jobs':
|
|
47
|
+
return await listJobsByTag(args, options);
|
|
48
|
+
default:
|
|
49
|
+
printError(`Unknown subcommand: ${subcommand}`);
|
|
50
|
+
printInfo('Available subcommands: list, add, rm, clear, rename, jobs');
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List all tags with job counts
|
|
57
|
+
* @param {object} options - Command options
|
|
58
|
+
* @returns {Promise<number>} Exit code
|
|
59
|
+
*/
|
|
60
|
+
async function listTags(options) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await send({
|
|
63
|
+
type: MessageType.TAG_LIST,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.type === MessageType.ERROR) {
|
|
67
|
+
printError(response.message);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (response.type === MessageType.TAG_LIST_RESULT) {
|
|
72
|
+
const tags = response.tags || {};
|
|
73
|
+
const tagNames = Object.keys(tags).sort();
|
|
74
|
+
|
|
75
|
+
if (tagNames.length === 0) {
|
|
76
|
+
printInfo('No tags found');
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
printHeader('Tags');
|
|
81
|
+
|
|
82
|
+
// Group by tag
|
|
83
|
+
for (const tagName of tagNames) {
|
|
84
|
+
const tagData = tags[tagName];
|
|
85
|
+
const jobCount = tagData.jobs?.length || 0;
|
|
86
|
+
console.log(` ${chalk.cyan(tagName)} ${chalk.gray(`(${jobCount} job${jobCount === 1 ? '' : 's'})`)}`);
|
|
87
|
+
|
|
88
|
+
if (options.verbose && tagData.jobs) {
|
|
89
|
+
for (const job of tagData.jobs) {
|
|
90
|
+
console.log(` - ${job.name || job.id} ${chalk.gray(`[${job.id}]`)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log();
|
|
96
|
+
printInfo(`${tagNames.length} tag${tagNames.length === 1 ? '' : 's'} found`);
|
|
97
|
+
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
printError('Unexpected response from daemon');
|
|
102
|
+
return 1;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
printError(`Failed to list tags: ${error.message}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add tags to jobs
|
|
111
|
+
* @param {string[]} args - [tagName, ...jobIds]
|
|
112
|
+
* @param {object} options - Command options
|
|
113
|
+
* @returns {Promise<number>} Exit code
|
|
114
|
+
*/
|
|
115
|
+
async function addTags(args, options) {
|
|
116
|
+
if (args.length < 2) {
|
|
117
|
+
printError('Usage: jm2 tags add <tag> <job-id-or-name> [...job-ids-or-names]');
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tagName = args[0];
|
|
122
|
+
const jobRefs = args.slice(1);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const response = await send({
|
|
126
|
+
type: MessageType.TAG_ADD,
|
|
127
|
+
tag: tagName,
|
|
128
|
+
jobRefs,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (response.type === MessageType.ERROR) {
|
|
132
|
+
printError(response.message);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (response.type === MessageType.TAG_ADD_RESULT) {
|
|
137
|
+
printSuccess(`Tag "${tagName}" added to ${response.count} job${response.count === 1 ? '' : 's'}`);
|
|
138
|
+
|
|
139
|
+
if (response.count > 0 && options.verbose) {
|
|
140
|
+
printInfo(`Updated job IDs: ${response.jobIds.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
printError('Unexpected response from daemon');
|
|
147
|
+
return 1;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
printError(`Failed to add tag: ${error.message}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove tags from jobs
|
|
156
|
+
* @param {string[]} args - [tagName, ...jobIds] or just [tagName] to remove from all
|
|
157
|
+
* @param {object} options - Command options
|
|
158
|
+
* @returns {Promise<number>} Exit code
|
|
159
|
+
*/
|
|
160
|
+
async function removeTags(args, options) {
|
|
161
|
+
if (args.length < 1) {
|
|
162
|
+
printError('Usage: jm2 tags rm <tag> [job-id-or-name ...]');
|
|
163
|
+
printInfo('Or: jm2 tags rm <tag> --all (to remove tag from all jobs)');
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tagName = args[0];
|
|
168
|
+
const jobRefs = args.length > 1 ? args.slice(1) : [];
|
|
169
|
+
|
|
170
|
+
// If --all flag is set or no specific jobs, remove from all
|
|
171
|
+
const removeFromAll = options.all || jobRefs.length === 0;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const response = await send({
|
|
175
|
+
type: MessageType.TAG_REMOVE,
|
|
176
|
+
tag: tagName,
|
|
177
|
+
jobRefs: removeFromAll ? null : jobRefs,
|
|
178
|
+
all: removeFromAll,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (response.type === MessageType.ERROR) {
|
|
182
|
+
printError(response.message);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (response.type === MessageType.TAG_REMOVE_RESULT) {
|
|
187
|
+
printSuccess(`Tag "${tagName}" removed from ${response.count} job${response.count === 1 ? '' : 's'}`);
|
|
188
|
+
|
|
189
|
+
if (response.count > 0 && options.verbose) {
|
|
190
|
+
printInfo(`Updated job IDs: ${response.jobIds.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
printError('Unexpected response from daemon');
|
|
197
|
+
return 1;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
printError(`Failed to remove tag: ${error.message}`);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Clear all tags from jobs
|
|
206
|
+
* @param {string[]} args - [...jobIds] or empty for all jobs
|
|
207
|
+
* @param {object} options - Command options
|
|
208
|
+
* @returns {Promise<number>} Exit code
|
|
209
|
+
*/
|
|
210
|
+
async function clearTags(args, options) {
|
|
211
|
+
const jobRefs = args.length > 0 ? args : null;
|
|
212
|
+
const clearAll = options.all || !jobRefs;
|
|
213
|
+
|
|
214
|
+
if (clearAll && !options.force) {
|
|
215
|
+
printError('This will clear all tags from all jobs. Use --force to confirm.');
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const response = await send({
|
|
221
|
+
type: MessageType.TAG_CLEAR,
|
|
222
|
+
jobRefs,
|
|
223
|
+
all: clearAll,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (response.type === MessageType.ERROR) {
|
|
227
|
+
printError(response.message);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (response.type === MessageType.TAG_CLEAR_RESULT) {
|
|
232
|
+
printSuccess(`Tags cleared from ${response.count} job${response.count === 1 ? '' : 's'}`);
|
|
233
|
+
|
|
234
|
+
if (response.count > 0 && options.verbose) {
|
|
235
|
+
printInfo(`Updated job IDs: ${response.jobIds.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
printError('Unexpected response from daemon');
|
|
242
|
+
return 1;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
printError(`Failed to clear tags: ${error.message}`);
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Rename a tag across all jobs
|
|
251
|
+
* @param {string[]} args - [oldTagName, newTagName]
|
|
252
|
+
* @param {object} options - Command options
|
|
253
|
+
* @returns {Promise<number>} Exit code
|
|
254
|
+
*/
|
|
255
|
+
async function renameTag(args, options) {
|
|
256
|
+
if (args.length !== 2) {
|
|
257
|
+
printError('Usage: jm2 tags rename <old-tag> <new-tag>');
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const [oldTag, newTag] = args;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await send({
|
|
265
|
+
type: MessageType.TAG_RENAME,
|
|
266
|
+
oldTag,
|
|
267
|
+
newTag,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (response.type === MessageType.ERROR) {
|
|
271
|
+
printError(response.message);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (response.type === MessageType.TAG_RENAME_RESULT) {
|
|
276
|
+
printSuccess(`Tag "${oldTag}" renamed to "${newTag}" in ${response.count} job${response.count === 1 ? '' : 's'}`);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
printError('Unexpected response from daemon');
|
|
281
|
+
return 1;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
printError(`Failed to rename tag: ${error.message}`);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* List jobs grouped by tag (including jobs with no tags)
|
|
290
|
+
* @param {string[]} args - Optional tag name to filter
|
|
291
|
+
* @param {object} options - Command options
|
|
292
|
+
* @returns {Promise<number>} Exit code
|
|
293
|
+
*/
|
|
294
|
+
async function listJobsByTag(args, options) {
|
|
295
|
+
const filterTag = args[0];
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const response = await send({
|
|
299
|
+
type: MessageType.TAG_LIST,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (response.type === MessageType.ERROR) {
|
|
303
|
+
printError(response.message);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (response.type === MessageType.TAG_LIST_RESULT) {
|
|
308
|
+
const tags = response.tags || {};
|
|
309
|
+
|
|
310
|
+
// If filtering by specific tag
|
|
311
|
+
if (filterTag) {
|
|
312
|
+
const tagData = tags[filterTag];
|
|
313
|
+
if (!tagData || !tagData.jobs || tagData.jobs.length === 0) {
|
|
314
|
+
printInfo(`No jobs found with tag "${filterTag}"`);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
printHeader(`Jobs with tag "${filterTag}"`);
|
|
319
|
+
printJobsTable(tagData.jobs);
|
|
320
|
+
console.log();
|
|
321
|
+
printInfo(`${tagData.jobs.length} job${tagData.jobs.length === 1 ? '' : 's'} found`);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Show all jobs grouped by tag
|
|
326
|
+
printHeader('Jobs by Tag');
|
|
327
|
+
|
|
328
|
+
const tagNames = Object.keys(tags).sort();
|
|
329
|
+
const noTagJobs = tags['(no tag)']?.jobs || [];
|
|
330
|
+
|
|
331
|
+
// First show tagged jobs
|
|
332
|
+
for (const tagName of tagNames) {
|
|
333
|
+
if (tagName === '(no tag)') continue;
|
|
334
|
+
|
|
335
|
+
const tagData = tags[tagName];
|
|
336
|
+
if (!tagData.jobs || tagData.jobs.length === 0) continue;
|
|
337
|
+
|
|
338
|
+
console.log(`\n${chalk.bold.cyan(tagName)} ${chalk.gray(`(${tagData.jobs.length} job${tagData.jobs.length === 1 ? '' : 's'})`)}`);
|
|
339
|
+
printJobsTable(tagData.jobs, true);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Then show jobs with no tags
|
|
343
|
+
if (noTagJobs.length > 0) {
|
|
344
|
+
console.log(`\n${chalk.bold.gray('(no tag)')} ${chalk.gray(`(${noTagJobs.length} job${noTagJobs.length === 1 ? '' : 's'})`)}`);
|
|
345
|
+
printJobsTable(noTagJobs, true);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log();
|
|
349
|
+
const totalJobs = Object.values(tags).reduce((sum, t) => sum + (t.jobs?.length || 0), 0);
|
|
350
|
+
const uniqueJobs = new Set();
|
|
351
|
+
Object.values(tags).forEach(t => t.jobs?.forEach(j => uniqueJobs.add(j.id)));
|
|
352
|
+
printInfo(`${uniqueJobs.size} job${uniqueJobs.size === 1 ? '' : 's'} found (${tagNames.length - (noTagJobs.length > 0 ? 1 : 0)} tag${tagNames.length === 1 ? '' : 's'})`);
|
|
353
|
+
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
printError('Unexpected response from daemon');
|
|
358
|
+
return 1;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
printError(`Failed to list jobs by tag: ${error.message}`);
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Print jobs in a compact table format
|
|
367
|
+
* @param {Array} jobs - Array of job objects
|
|
368
|
+
* @param {boolean} compact - Whether to use compact format
|
|
369
|
+
*/
|
|
370
|
+
function printJobsTable(jobs, compact = false) {
|
|
371
|
+
const table = createJobTable();
|
|
372
|
+
|
|
373
|
+
for (const job of jobs) {
|
|
374
|
+
const schedule = job.cron
|
|
375
|
+
? chalk.gray(job.cron)
|
|
376
|
+
: job.runAt
|
|
377
|
+
? formatRelativeTime(job.runAt)
|
|
378
|
+
: chalk.gray('Manual');
|
|
379
|
+
|
|
380
|
+
if (compact) {
|
|
381
|
+
console.log(` ${job.id} ${job.name || chalk.gray('-')} ${colorizeStatus(job.status)} ${schedule}`);
|
|
382
|
+
} else {
|
|
383
|
+
table.push([
|
|
384
|
+
job.id,
|
|
385
|
+
job.name || chalk.gray('-'),
|
|
386
|
+
colorizeStatus(job.status),
|
|
387
|
+
schedule,
|
|
388
|
+
formatRelativeTime(job.nextRun),
|
|
389
|
+
formatRelativeTime(job.lastRun),
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!compact && jobs.length > 0) {
|
|
395
|
+
console.log(table.toString());
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export default { tagsCommand };
|
package/src/cli/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { exportCommand } from './commands/export.js';
|
|
|
28
28
|
import { importCommand } from './commands/import.js';
|
|
29
29
|
import { installCommand } from './commands/install.js';
|
|
30
30
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
31
|
+
import { tagsCommand } from './commands/tags.js';
|
|
31
32
|
|
|
32
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
34
|
const __dirname = dirname(__filename);
|
|
@@ -191,6 +192,8 @@ export async function runCli() {
|
|
|
191
192
|
.option('--timeout <duration>', 'New timeout for job execution')
|
|
192
193
|
.option('--retry <count>', 'New retry count on failure')
|
|
193
194
|
.option('-t, --tag <tag>', 'Set tags (replaces all existing tags, can be used multiple times)', collect, [])
|
|
195
|
+
.option('--tag-append <tag>', 'Append tags to existing tags (can be used multiple times)', collect, [])
|
|
196
|
+
.option('--tag-remove <tag>', 'Remove tags from existing tags (can be used multiple times)', collect, [])
|
|
194
197
|
.action(async (job, options) => {
|
|
195
198
|
const exitCode = await editCommand(job, options);
|
|
196
199
|
process.exit(exitCode);
|
|
@@ -296,6 +299,31 @@ export async function runCli() {
|
|
|
296
299
|
process.exit(exitCode);
|
|
297
300
|
});
|
|
298
301
|
|
|
302
|
+
// Tags command
|
|
303
|
+
program
|
|
304
|
+
.command('tags <subcommand> [args...]')
|
|
305
|
+
.description('Manage job tags')
|
|
306
|
+
.option('-v, --verbose', 'Show verbose output', false)
|
|
307
|
+
.option('-a, --all', 'Apply to all jobs', false)
|
|
308
|
+
.option('-f, --force', 'Skip confirmation for destructive operations', false)
|
|
309
|
+
.addHelpText('after', `
|
|
310
|
+
Examples:
|
|
311
|
+
jm2 tags list List all tags with job counts
|
|
312
|
+
jm2 tags list -v List tags with associated jobs
|
|
313
|
+
jm2 tags add production 1 2 3 Add "production" tag to jobs 1, 2, 3
|
|
314
|
+
jm2 tags rm staging 1 2 Remove "staging" tag from jobs 1, 2
|
|
315
|
+
jm2 tags rm old-tag --all Remove "old-tag" from all jobs
|
|
316
|
+
jm2 tags clear 1 2 Clear all tags from jobs 1, 2
|
|
317
|
+
jm2 tags clear --all --force Clear all tags from all jobs
|
|
318
|
+
jm2 tags rename old new Rename tag "old" to "new"
|
|
319
|
+
jm2 tags jobs Show all jobs grouped by tag
|
|
320
|
+
jm2 tags jobs production Show jobs with "production" tag
|
|
321
|
+
`)
|
|
322
|
+
.action(async (subcommand, args, options) => {
|
|
323
|
+
const exitCode = await tagsCommand(subcommand, args || [], options);
|
|
324
|
+
process.exit(exitCode);
|
|
325
|
+
});
|
|
326
|
+
|
|
299
327
|
// Parse command line arguments
|
|
300
328
|
await program.parseAsync();
|
|
301
329
|
}
|
package/src/daemon/index.js
CHANGED
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
createJobResumedResponse,
|
|
27
27
|
createJobRunResponse,
|
|
28
28
|
createFlushResultResponse,
|
|
29
|
+
createTagListResponse,
|
|
30
|
+
createTagAddResponse,
|
|
31
|
+
createTagRemoveResponse,
|
|
32
|
+
createTagClearResponse,
|
|
33
|
+
createTagRenameResponse,
|
|
29
34
|
} from '../ipc/protocol.js';
|
|
30
35
|
|
|
31
36
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -332,6 +337,21 @@ async function handleIpcMessage(message) {
|
|
|
332
337
|
case MessageType.RELOAD_JOBS:
|
|
333
338
|
return handleReloadJobs(message);
|
|
334
339
|
|
|
340
|
+
case MessageType.TAG_LIST:
|
|
341
|
+
return handleTagList(message);
|
|
342
|
+
|
|
343
|
+
case MessageType.TAG_ADD:
|
|
344
|
+
return handleTagAdd(message);
|
|
345
|
+
|
|
346
|
+
case MessageType.TAG_REMOVE:
|
|
347
|
+
return handleTagRemove(message);
|
|
348
|
+
|
|
349
|
+
case MessageType.TAG_CLEAR:
|
|
350
|
+
return handleTagClear(message);
|
|
351
|
+
|
|
352
|
+
case MessageType.TAG_RENAME:
|
|
353
|
+
return handleTagRename(message);
|
|
354
|
+
|
|
335
355
|
default:
|
|
336
356
|
return {
|
|
337
357
|
type: 'error',
|
|
@@ -763,6 +783,8 @@ function handleFlush(message) {
|
|
|
763
783
|
result.jobsRemoved = initialCount - filteredJobs.length;
|
|
764
784
|
if (result.jobsRemoved > 0) {
|
|
765
785
|
saveJobs(filteredJobs);
|
|
786
|
+
// Reload jobs into scheduler to sync in-memory state with storage
|
|
787
|
+
scheduler.loadJobs();
|
|
766
788
|
logger?.info(`Flushed ${result.jobsRemoved} completed one-time jobs`);
|
|
767
789
|
}
|
|
768
790
|
}
|
|
@@ -828,6 +850,231 @@ function handleFlush(message) {
|
|
|
828
850
|
}
|
|
829
851
|
}
|
|
830
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Handle tag list message
|
|
855
|
+
* Returns all tags grouped with their jobs
|
|
856
|
+
* @returns {object} Response
|
|
857
|
+
*/
|
|
858
|
+
function handleTagList() {
|
|
859
|
+
try {
|
|
860
|
+
const jobs = scheduler.getAllJobs();
|
|
861
|
+
const tags = {};
|
|
862
|
+
|
|
863
|
+
// Group jobs by tag
|
|
864
|
+
for (const job of jobs) {
|
|
865
|
+
const jobTags = job.tags || [];
|
|
866
|
+
|
|
867
|
+
if (jobTags.length === 0) {
|
|
868
|
+
// Jobs with no tags
|
|
869
|
+
if (!tags['(no tag)']) {
|
|
870
|
+
tags['(no tag)'] = { jobs: [] };
|
|
871
|
+
}
|
|
872
|
+
tags['(no tag)'].jobs.push(job);
|
|
873
|
+
} else {
|
|
874
|
+
// Jobs with tags (can be in multiple groups)
|
|
875
|
+
for (const tag of jobTags) {
|
|
876
|
+
if (!tags[tag]) {
|
|
877
|
+
tags[tag] = { jobs: [] };
|
|
878
|
+
}
|
|
879
|
+
tags[tag].jobs.push(job);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return createTagListResponse(tags);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
logger?.error(`Failed to list tags: ${error.message}`);
|
|
887
|
+
return {
|
|
888
|
+
type: MessageType.ERROR,
|
|
889
|
+
message: `Failed to list tags: ${error.message}`,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Handle tag add message
|
|
896
|
+
* Adds a tag to specified jobs
|
|
897
|
+
* @param {object} message - Message with tag and jobRefs
|
|
898
|
+
* @returns {object} Response
|
|
899
|
+
*/
|
|
900
|
+
function handleTagAdd(message) {
|
|
901
|
+
try {
|
|
902
|
+
const { tag, jobRefs } = message;
|
|
903
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
904
|
+
|
|
905
|
+
const jobs = scheduler.getAllJobs();
|
|
906
|
+
const updatedJobIds = [];
|
|
907
|
+
|
|
908
|
+
for (const jobRef of jobRefs) {
|
|
909
|
+
// Find job by ID or name
|
|
910
|
+
let job = null;
|
|
911
|
+
const jobId = parseInt(jobRef, 10);
|
|
912
|
+
|
|
913
|
+
if (!isNaN(jobId)) {
|
|
914
|
+
job = jobs.find(j => j.id === jobId);
|
|
915
|
+
}
|
|
916
|
+
if (!job) {
|
|
917
|
+
job = jobs.find(j => j.name === jobRef);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (job) {
|
|
921
|
+
const currentTags = job.tags || [];
|
|
922
|
+
if (!currentTags.includes(normalizedTag)) {
|
|
923
|
+
const newTags = [...currentTags, normalizedTag];
|
|
924
|
+
scheduler.updateJob(job.id, { tags: newTags });
|
|
925
|
+
updatedJobIds.push(job.id);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (updatedJobIds.length > 0) {
|
|
931
|
+
logger?.info(`Added tag "${normalizedTag}" to ${updatedJobIds.length} jobs`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return createTagAddResponse(updatedJobIds.length, updatedJobIds);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
logger?.error(`Failed to add tag: ${error.message}`);
|
|
937
|
+
return {
|
|
938
|
+
type: MessageType.ERROR,
|
|
939
|
+
message: `Failed to add tag: ${error.message}`,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Handle tag remove message
|
|
946
|
+
* Removes a tag from specified jobs or all jobs
|
|
947
|
+
* @param {object} message - Message with tag, jobRefs, and all flag
|
|
948
|
+
* @returns {object} Response
|
|
949
|
+
*/
|
|
950
|
+
function handleTagRemove(message) {
|
|
951
|
+
try {
|
|
952
|
+
const { tag, jobRefs, all } = message;
|
|
953
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
954
|
+
|
|
955
|
+
const jobs = scheduler.getAllJobs();
|
|
956
|
+
const updatedJobIds = [];
|
|
957
|
+
|
|
958
|
+
// Determine which jobs to process
|
|
959
|
+
const jobsToProcess = all
|
|
960
|
+
? jobs
|
|
961
|
+
: jobRefs.map(ref => {
|
|
962
|
+
const jobId = parseInt(ref, 10);
|
|
963
|
+
if (!isNaN(jobId)) {
|
|
964
|
+
return jobs.find(j => j.id === jobId) || jobs.find(j => j.name === ref);
|
|
965
|
+
}
|
|
966
|
+
return jobs.find(j => j.name === ref);
|
|
967
|
+
}).filter(Boolean);
|
|
968
|
+
|
|
969
|
+
for (const job of jobsToProcess) {
|
|
970
|
+
const currentTags = job.tags || [];
|
|
971
|
+
if (currentTags.includes(normalizedTag)) {
|
|
972
|
+
const newTags = currentTags.filter(t => t !== normalizedTag);
|
|
973
|
+
scheduler.updateJob(job.id, { tags: newTags });
|
|
974
|
+
updatedJobIds.push(job.id);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (updatedJobIds.length > 0) {
|
|
979
|
+
logger?.info(`Removed tag "${normalizedTag}" from ${updatedJobIds.length} jobs`);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return createTagRemoveResponse(updatedJobIds.length, updatedJobIds);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
logger?.error(`Failed to remove tag: ${error.message}`);
|
|
985
|
+
return {
|
|
986
|
+
type: MessageType.ERROR,
|
|
987
|
+
message: `Failed to remove tag: ${error.message}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Handle tag clear message
|
|
994
|
+
* Clears all tags from specified jobs or all jobs
|
|
995
|
+
* @param {object} message - Message with jobRefs and all flag
|
|
996
|
+
* @returns {object} Response
|
|
997
|
+
*/
|
|
998
|
+
function handleTagClear(message) {
|
|
999
|
+
try {
|
|
1000
|
+
const { jobRefs, all } = message;
|
|
1001
|
+
|
|
1002
|
+
const jobs = scheduler.getAllJobs();
|
|
1003
|
+
const updatedJobIds = [];
|
|
1004
|
+
|
|
1005
|
+
// Determine which jobs to process
|
|
1006
|
+
const jobsToProcess = all
|
|
1007
|
+
? jobs
|
|
1008
|
+
: (jobRefs || []).map(ref => {
|
|
1009
|
+
const jobId = parseInt(ref, 10);
|
|
1010
|
+
if (!isNaN(jobId)) {
|
|
1011
|
+
return jobs.find(j => j.id === jobId) || jobs.find(j => j.name === ref);
|
|
1012
|
+
}
|
|
1013
|
+
return jobs.find(j => j.name === ref);
|
|
1014
|
+
}).filter(Boolean);
|
|
1015
|
+
|
|
1016
|
+
for (const job of jobsToProcess) {
|
|
1017
|
+
const currentTags = job.tags || [];
|
|
1018
|
+
if (currentTags.length > 0) {
|
|
1019
|
+
scheduler.updateJob(job.id, { tags: [] });
|
|
1020
|
+
updatedJobIds.push(job.id);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (updatedJobIds.length > 0) {
|
|
1025
|
+
logger?.info(`Cleared all tags from ${updatedJobIds.length} jobs`);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return createTagClearResponse(updatedJobIds.length, updatedJobIds);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
logger?.error(`Failed to clear tags: ${error.message}`);
|
|
1031
|
+
return {
|
|
1032
|
+
type: MessageType.ERROR,
|
|
1033
|
+
message: `Failed to clear tags: ${error.message}`,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Handle tag rename message
|
|
1040
|
+
* Renames a tag across all jobs
|
|
1041
|
+
* @param {object} message - Message with oldTag and newTag
|
|
1042
|
+
* @returns {object} Response
|
|
1043
|
+
*/
|
|
1044
|
+
function handleTagRename(message) {
|
|
1045
|
+
try {
|
|
1046
|
+
const { oldTag, newTag } = message;
|
|
1047
|
+
const normalizedOldTag = oldTag.trim().toLowerCase();
|
|
1048
|
+
const normalizedNewTag = newTag.trim().toLowerCase();
|
|
1049
|
+
|
|
1050
|
+
const jobs = scheduler.getAllJobs();
|
|
1051
|
+
let updatedCount = 0;
|
|
1052
|
+
|
|
1053
|
+
for (const job of jobs) {
|
|
1054
|
+
const currentTags = job.tags || [];
|
|
1055
|
+
if (currentTags.includes(normalizedOldTag)) {
|
|
1056
|
+
const newTags = currentTags.map(t => t === normalizedOldTag ? normalizedNewTag : t);
|
|
1057
|
+
// Remove duplicates that might result from rename
|
|
1058
|
+
const uniqueTags = [...new Set(newTags)];
|
|
1059
|
+
scheduler.updateJob(job.id, { tags: uniqueTags });
|
|
1060
|
+
updatedCount++;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (updatedCount > 0) {
|
|
1065
|
+
logger?.info(`Renamed tag "${normalizedOldTag}" to "${normalizedNewTag}" in ${updatedCount} jobs`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return createTagRenameResponse(updatedCount);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
logger?.error(`Failed to rename tag: ${error.message}`);
|
|
1071
|
+
return {
|
|
1072
|
+
type: MessageType.ERROR,
|
|
1073
|
+
message: `Failed to rename tag: ${error.message}`,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
831
1078
|
/**
|
|
832
1079
|
* Handle reload jobs message
|
|
833
1080
|
* Reloads jobs from storage into scheduler
|
package/src/daemon/scheduler.js
CHANGED
|
@@ -335,9 +335,34 @@ export class Scheduler {
|
|
|
335
335
|
return null;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// Handle tag append/remove operations
|
|
339
|
+
let finalTags = job.tags || [];
|
|
340
|
+
|
|
341
|
+
if (updates.tagsAppend && updates.tagsAppend.length > 0) {
|
|
342
|
+
// Normalize and append new tags
|
|
343
|
+
const tagsToAppend = updates.tagsAppend.map(t => t.trim().toLowerCase());
|
|
344
|
+
finalTags = [...new Set([...finalTags, ...tagsToAppend])];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (updates.tagsRemove && updates.tagsRemove.length > 0) {
|
|
348
|
+
// Normalize and remove tags
|
|
349
|
+
const tagsToRemove = updates.tagsRemove.map(t => t.trim().toLowerCase());
|
|
350
|
+
finalTags = finalTags.filter(t => !tagsToRemove.includes(t));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Create a clean updates object without the append/remove markers
|
|
354
|
+
const cleanUpdates = { ...updates };
|
|
355
|
+
delete cleanUpdates.tagsAppend;
|
|
356
|
+
delete cleanUpdates.tagsRemove;
|
|
357
|
+
|
|
358
|
+
// If we modified tags via append/remove, update the tags field
|
|
359
|
+
if (updates.tagsAppend || updates.tagsRemove) {
|
|
360
|
+
cleanUpdates.tags = finalTags;
|
|
361
|
+
}
|
|
362
|
+
|
|
338
363
|
const updatedJob = {
|
|
339
364
|
...job,
|
|
340
|
-
...
|
|
365
|
+
...cleanUpdates,
|
|
341
366
|
id: jobId, // Don't allow changing ID
|
|
342
367
|
updatedAt: new Date().toISOString(),
|
|
343
368
|
};
|
package/src/ipc/protocol.js
CHANGED
|
@@ -26,6 +26,18 @@ export const MessageType = {
|
|
|
26
26
|
JOB_RUN: 'job:run',
|
|
27
27
|
JOB_RUN_RESULT: 'job:run:result',
|
|
28
28
|
|
|
29
|
+
// Tag management
|
|
30
|
+
TAG_LIST: 'tag:list',
|
|
31
|
+
TAG_LIST_RESULT: 'tag:list:result',
|
|
32
|
+
TAG_ADD: 'tag:add',
|
|
33
|
+
TAG_ADD_RESULT: 'tag:add:result',
|
|
34
|
+
TAG_REMOVE: 'tag:remove',
|
|
35
|
+
TAG_REMOVE_RESULT: 'tag:remove:result',
|
|
36
|
+
TAG_CLEAR: 'tag:clear',
|
|
37
|
+
TAG_CLEAR_RESULT: 'tag:clear:result',
|
|
38
|
+
TAG_RENAME: 'tag:rename',
|
|
39
|
+
TAG_RENAME_RESULT: 'tag:rename:result',
|
|
40
|
+
|
|
29
41
|
// Flush/cleanup
|
|
30
42
|
FLUSH: 'flush',
|
|
31
43
|
FLUSH_RESULT: 'flush:result',
|
|
@@ -145,6 +157,72 @@ export function createFlushResultResponse(result) {
|
|
|
145
157
|
};
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Create a tag list response
|
|
162
|
+
* @param {object} tags - Tags grouped by name with job counts
|
|
163
|
+
* @returns {{ type: string, tags: object }}
|
|
164
|
+
*/
|
|
165
|
+
export function createTagListResponse(tags) {
|
|
166
|
+
return {
|
|
167
|
+
type: MessageType.TAG_LIST_RESULT,
|
|
168
|
+
tags,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a tag add response
|
|
174
|
+
* @param {number} count - Number of jobs updated
|
|
175
|
+
* @param {string[]} jobIds - IDs of jobs that were updated
|
|
176
|
+
* @returns {{ type: string, count: number, jobIds: string[] }}
|
|
177
|
+
*/
|
|
178
|
+
export function createTagAddResponse(count, jobIds) {
|
|
179
|
+
return {
|
|
180
|
+
type: MessageType.TAG_ADD_RESULT,
|
|
181
|
+
count,
|
|
182
|
+
jobIds,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a tag remove response
|
|
188
|
+
* @param {number} count - Number of jobs updated
|
|
189
|
+
* @param {string[]} jobIds - IDs of jobs that were updated
|
|
190
|
+
* @returns {{ type: string, count: number, jobIds: string[] }}
|
|
191
|
+
*/
|
|
192
|
+
export function createTagRemoveResponse(count, jobIds) {
|
|
193
|
+
return {
|
|
194
|
+
type: MessageType.TAG_REMOVE_RESULT,
|
|
195
|
+
count,
|
|
196
|
+
jobIds,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a tag clear response
|
|
202
|
+
* @param {number} count - Number of jobs updated
|
|
203
|
+
* @param {string[]} jobIds - IDs of jobs that were updated
|
|
204
|
+
* @returns {{ type: string, count: number, jobIds: string[] }}
|
|
205
|
+
*/
|
|
206
|
+
export function createTagClearResponse(count, jobIds) {
|
|
207
|
+
return {
|
|
208
|
+
type: MessageType.TAG_CLEAR_RESULT,
|
|
209
|
+
count,
|
|
210
|
+
jobIds,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a tag rename response
|
|
216
|
+
* @param {number} count - Number of jobs updated
|
|
217
|
+
* @returns {{ type: string, count: number }}
|
|
218
|
+
*/
|
|
219
|
+
export function createTagRenameResponse(count) {
|
|
220
|
+
return {
|
|
221
|
+
type: MessageType.TAG_RENAME_RESULT,
|
|
222
|
+
count,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
148
226
|
/**
|
|
149
227
|
* Create a standard error response
|
|
150
228
|
* @param {string} message - Error message
|
|
@@ -180,4 +258,9 @@ export default {
|
|
|
180
258
|
createJobResumedResponse,
|
|
181
259
|
createJobRunResponse,
|
|
182
260
|
createFlushResultResponse,
|
|
261
|
+
createTagListResponse,
|
|
262
|
+
createTagAddResponse,
|
|
263
|
+
createTagRemoveResponse,
|
|
264
|
+
createTagClearResponse,
|
|
265
|
+
createTagRenameResponse,
|
|
183
266
|
};
|
package/src/ipc/server.js
CHANGED
|
@@ -17,13 +17,19 @@ export function startIpcServer(options = {}) {
|
|
|
17
17
|
const { onMessage } = options;
|
|
18
18
|
const socketPath = getSocketPath();
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
unlinkSync(socketPath);
|
|
22
|
-
}
|
|
23
|
-
|
|
20
|
+
// Ensure directories exist first
|
|
24
21
|
ensureDataDir();
|
|
25
22
|
ensureRuntimeDir();
|
|
26
23
|
|
|
24
|
+
// Clean up stale socket file
|
|
25
|
+
if (process.platform !== 'win32' && existsSync(socketPath)) {
|
|
26
|
+
try {
|
|
27
|
+
unlinkSync(socketPath);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Failed to remove stale socket file: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
const server = createServer(socket => {
|
|
28
34
|
let buffer = '';
|
|
29
35
|
|
|
@@ -72,7 +78,21 @@ export function startIpcServer(options = {}) {
|
|
|
72
78
|
});
|
|
73
79
|
});
|
|
74
80
|
|
|
81
|
+
// Add error handler for the server
|
|
82
|
+
server.on('error', (error) => {
|
|
83
|
+
throw new Error(`IPC server error: ${error.message}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
75
86
|
server.listen(socketPath);
|
|
87
|
+
|
|
88
|
+
// Verify socket was created
|
|
89
|
+
server.on('listening', () => {
|
|
90
|
+
if (process.platform !== 'win32' && !existsSync(socketPath)) {
|
|
91
|
+
server.close();
|
|
92
|
+
throw new Error(`Socket file was not created at ${socketPath}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
76
96
|
return server;
|
|
77
97
|
}
|
|
78
98
|
|
|
@@ -85,6 +105,18 @@ export function stopIpcServer(server) {
|
|
|
85
105
|
return;
|
|
86
106
|
}
|
|
87
107
|
server.close();
|
|
108
|
+
|
|
109
|
+
// Clean up socket file on Unix systems
|
|
110
|
+
if (process.platform !== 'win32') {
|
|
111
|
+
const socketPath = getSocketPath();
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(socketPath)) {
|
|
114
|
+
unlinkSync(socketPath);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Ignore errors during cleanup
|
|
118
|
+
}
|
|
119
|
+
}
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
export default {
|
package/src/utils/paths.js
CHANGED
|
@@ -16,7 +16,7 @@ const DATA_DIR_NAME = '.jm2';
|
|
|
16
16
|
* Get the runtime directory for sockets
|
|
17
17
|
* Uses platform-specific standard locations:
|
|
18
18
|
* - Linux: /run/user/<uid>/jm2/ (XDG standard)
|
|
19
|
-
* - macOS:
|
|
19
|
+
* - macOS: ~/.jm2/ (Caches directory gets cleaned up by system)
|
|
20
20
|
* - Others: ~/.jm2/
|
|
21
21
|
* @returns {string} The runtime directory path
|
|
22
22
|
*/
|
|
@@ -33,12 +33,8 @@ function getRuntimeDir() {
|
|
|
33
33
|
return join(xdgRuntimeDir, 'jm2');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return join(homedir(), 'Library', 'Caches', 'jm2');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Fallback to data directory for other platforms
|
|
36
|
+
// For macOS and other platforms, use data directory
|
|
37
|
+
// Note: Library/Caches gets cleaned up by macOS, causing socket files to disappear
|
|
42
38
|
return getDataDir();
|
|
43
39
|
}
|
|
44
40
|
|