@coreify/quitx 1.0.0 → 1.2.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.
- package/CHANGELOG.md +42 -0
- package/dist/cli.js +1192 -212
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spinner, select, isCancel, log, intro, outro, multiselect, confirm, cancel } from '@clack/prompts';
|
|
3
|
+
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
4
4
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
-
import { join } from 'path';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
7
|
import { execa } from 'execa';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
|
-
import { MultiSelectPrompt } from '@clack/core';
|
|
10
9
|
|
|
11
10
|
var CONFIG_DIR = join(homedir(), ".quitx");
|
|
12
11
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -14,27 +13,43 @@ var DEFAULT_CONFIG = {
|
|
|
14
13
|
exclude: [],
|
|
15
14
|
force: "normal",
|
|
16
15
|
includeFinder: false,
|
|
17
|
-
|
|
16
|
+
includeTrash: false,
|
|
17
|
+
includeBackground: false,
|
|
18
|
+
groupBackground: true,
|
|
19
|
+
defaultSelectAll: true,
|
|
20
|
+
neverQuitMusic: false,
|
|
21
|
+
musicApps: [],
|
|
22
|
+
autoUpdate: true
|
|
18
23
|
};
|
|
19
24
|
function loadConfig(filePath = CONFIG_FILE) {
|
|
20
25
|
try {
|
|
21
26
|
if (!existsSync(filePath)) {
|
|
22
|
-
return { ...DEFAULT_CONFIG, exclude: [] };
|
|
27
|
+
return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
|
|
23
28
|
}
|
|
24
29
|
const raw = readFileSync(filePath, "utf-8");
|
|
25
30
|
const parsed = JSON.parse(raw);
|
|
26
31
|
if (typeof parsed !== "object" || parsed === null) {
|
|
27
|
-
return { ...DEFAULT_CONFIG, exclude: [] };
|
|
32
|
+
return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
|
|
28
33
|
}
|
|
29
34
|
const obj = parsed;
|
|
35
|
+
const defaultSelectVal = typeof obj.defaultSelectAll === "boolean" ? obj.defaultSelectAll : obj.defaultSelect === "deselect-all" || obj.defaultSelection === "deselect-all" || obj.defaultSelect === false ? false : typeof obj.defaultSelect === "boolean" ? obj.defaultSelect : DEFAULT_CONFIG.defaultSelectAll;
|
|
36
|
+
const groupBgVal = typeof obj.groupBackground === "boolean" ? obj.groupBackground : typeof obj.groupBackgroundInstances === "boolean" ? obj.groupBackgroundInstances : DEFAULT_CONFIG.groupBackground;
|
|
37
|
+
const neverQuitMusicVal = typeof obj.neverQuitMusic === "boolean" ? obj.neverQuitMusic : DEFAULT_CONFIG.neverQuitMusic;
|
|
38
|
+
const autoUpdateVal = typeof obj.autoUpdate === "boolean" ? obj.autoUpdate : typeof obj.checkUpdate === "boolean" ? obj.checkUpdate : DEFAULT_CONFIG.autoUpdate;
|
|
30
39
|
return {
|
|
31
40
|
exclude: Array.isArray(obj.exclude) ? obj.exclude : [],
|
|
32
41
|
force: obj.force === "force" || obj.force === "normal" ? obj.force : DEFAULT_CONFIG.force,
|
|
33
42
|
includeFinder: typeof obj.includeFinder === "boolean" ? obj.includeFinder : DEFAULT_CONFIG.includeFinder,
|
|
34
|
-
|
|
43
|
+
includeTrash: typeof obj.includeTrash === "boolean" ? obj.includeTrash : DEFAULT_CONFIG.includeTrash,
|
|
44
|
+
includeBackground: typeof obj.includeBackground === "boolean" ? obj.includeBackground : DEFAULT_CONFIG.includeBackground,
|
|
45
|
+
groupBackground: groupBgVal,
|
|
46
|
+
defaultSelectAll: defaultSelectVal,
|
|
47
|
+
neverQuitMusic: neverQuitMusicVal,
|
|
48
|
+
musicApps: Array.isArray(obj.musicApps) ? obj.musicApps : DEFAULT_CONFIG.musicApps,
|
|
49
|
+
autoUpdate: autoUpdateVal
|
|
35
50
|
};
|
|
36
51
|
} catch {
|
|
37
|
-
return { ...DEFAULT_CONFIG, exclude: [] };
|
|
52
|
+
return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
55
|
function saveConfig(config, filePath = CONFIG_FILE) {
|
|
@@ -72,6 +87,31 @@ function removeExcludedApps(apps, filePath = CONFIG_FILE) {
|
|
|
72
87
|
saveConfig(next, filePath);
|
|
73
88
|
return next;
|
|
74
89
|
}
|
|
90
|
+
function addMusicApps(apps, filePath = CONFIG_FILE) {
|
|
91
|
+
const current = loadConfig(filePath);
|
|
92
|
+
const set = new Set(current.musicApps.map((a) => a.toLowerCase().trim()));
|
|
93
|
+
const updated = [...current.musicApps];
|
|
94
|
+
for (const app of apps) {
|
|
95
|
+
const trimmed = app.trim();
|
|
96
|
+
if (trimmed && !set.has(trimmed.toLowerCase())) {
|
|
97
|
+
set.add(trimmed.toLowerCase());
|
|
98
|
+
updated.push(trimmed);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const next = { ...current, musicApps: updated };
|
|
102
|
+
saveConfig(next, filePath);
|
|
103
|
+
return next;
|
|
104
|
+
}
|
|
105
|
+
function removeMusicApps(apps, filePath = CONFIG_FILE) {
|
|
106
|
+
const current = loadConfig(filePath);
|
|
107
|
+
const toRemove = new Set(apps.map((a) => a.toLowerCase().trim()));
|
|
108
|
+
const updated = current.musicApps.filter(
|
|
109
|
+
(app) => !toRemove.has(app.toLowerCase().trim())
|
|
110
|
+
);
|
|
111
|
+
const next = { ...current, musicApps: updated };
|
|
112
|
+
saveConfig(next, filePath);
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
75
115
|
|
|
76
116
|
// src/macos/jxa.ts
|
|
77
117
|
var APPKIT_PREAMBLE = "ObjC.import('AppKit');";
|
|
@@ -98,6 +138,10 @@ async function runJXA(script, args = [], executor = execa) {
|
|
|
98
138
|
]);
|
|
99
139
|
return stdout.trim();
|
|
100
140
|
}
|
|
141
|
+
async function runAppleScript(script, executor = execa) {
|
|
142
|
+
const { stdout } = await executor("osascript", ["-e", script]);
|
|
143
|
+
return stdout.trim();
|
|
144
|
+
}
|
|
101
145
|
|
|
102
146
|
// src/macos/apps.ts
|
|
103
147
|
function buildDiscoveryScript(includeBackground = false) {
|
|
@@ -114,7 +158,18 @@ function run() {
|
|
|
114
158
|
const name = app.localizedName ? app.localizedName.js : '';
|
|
115
159
|
const bundleId = app.bundleIdentifier ? app.bundleIdentifier.js : '';
|
|
116
160
|
const pid = String(app.processIdentifier);
|
|
117
|
-
|
|
161
|
+
const isBg = app.activationPolicy !== Regular ? '1' : '0';
|
|
162
|
+
let isMusic = '0';
|
|
163
|
+
if (app.bundleURL) {
|
|
164
|
+
const bundle = $.NSBundle.bundleWithURL(app.bundleURL);
|
|
165
|
+
if (bundle && bundle.infoDictionary) {
|
|
166
|
+
const cat = bundle.infoDictionary.objectForKey('LSApplicationCategoryType');
|
|
167
|
+
if (cat && cat.js === 'public.app-category.music') {
|
|
168
|
+
isMusic = '1';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push(name + '\\t' + bundleId + '\\t' + pid + '\\t' + isBg + '\\t' + isMusic);
|
|
118
173
|
}
|
|
119
174
|
return lines.join('\\n');
|
|
120
175
|
}`.trim();
|
|
@@ -148,35 +203,104 @@ function parseAppListOutput(stdout) {
|
|
|
148
203
|
const results = [];
|
|
149
204
|
for (const line of lines) {
|
|
150
205
|
const parts = line.split(" ");
|
|
151
|
-
const rawName = parts[0]?.trim();
|
|
206
|
+
const rawName = parts[0]?.replace(/[\u200B-\u200D\u200E\u200F\uFEFF]/g, "").trim();
|
|
152
207
|
if (!rawName) continue;
|
|
153
208
|
const rawBundle = parts[1]?.trim();
|
|
154
209
|
const bundleId = rawBundle && rawBundle !== "missing value" && rawBundle.length > 0 ? rawBundle : void 0;
|
|
155
210
|
const rawPid = parts[2]?.trim();
|
|
156
211
|
const pidNum = rawPid ? parseInt(rawPid, 10) : void 0;
|
|
157
212
|
const pid = !Number.isNaN(pidNum) && pidNum !== void 0 && pidNum > 0 ? pidNum : void 0;
|
|
213
|
+
const rawIsBg = parts[3]?.trim();
|
|
214
|
+
const isBackground = rawIsBg === "1";
|
|
215
|
+
const rawIsMusic = parts[4]?.trim();
|
|
216
|
+
const isMusic = rawIsMusic === "1";
|
|
158
217
|
let name = rawName;
|
|
159
218
|
if (bundleId && rawName === bundleId) {
|
|
160
219
|
name = formatDynamicName(bundleId);
|
|
161
220
|
}
|
|
162
|
-
results.push({
|
|
221
|
+
results.push({
|
|
222
|
+
name,
|
|
223
|
+
bundleId,
|
|
224
|
+
pid,
|
|
225
|
+
...isBackground ? { isBackground: true } : {},
|
|
226
|
+
...isMusic ? { isMusic: true } : {}
|
|
227
|
+
});
|
|
163
228
|
}
|
|
164
229
|
return results;
|
|
165
230
|
}
|
|
166
|
-
return trimmed.split(",").map(
|
|
231
|
+
return trimmed.split(",").map(
|
|
232
|
+
(name) => name.replace(/[\u200B-\u200D\u200E\u200F\uFEFF]/g, "").trim()
|
|
233
|
+
).filter(Boolean).map((name) => ({ name }));
|
|
167
234
|
}
|
|
168
235
|
function sortApps(apps) {
|
|
169
236
|
return [...apps].sort(
|
|
170
237
|
(a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
|
|
171
238
|
);
|
|
172
239
|
}
|
|
240
|
+
var KNOWN_MUSIC_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
241
|
+
"com.apple.music",
|
|
242
|
+
"com.spotify.client",
|
|
243
|
+
"com.tidal.desktop",
|
|
244
|
+
"com.github.th-ch.youtube-music",
|
|
245
|
+
"app.ytmdesktop.ytmdesktop",
|
|
246
|
+
"com.amazon.music",
|
|
247
|
+
"com.deezer.deezer-desktop",
|
|
248
|
+
"com.coppertino.vox",
|
|
249
|
+
"com.audirvana.audirvana-plus",
|
|
250
|
+
"com.audirvana.audirvana",
|
|
251
|
+
"com.swinsian.swinsian",
|
|
252
|
+
"cider.sh",
|
|
253
|
+
"com.cider.cider"
|
|
254
|
+
]);
|
|
255
|
+
var KNOWN_MUSIC_APP_NAMES = /* @__PURE__ */ new Set([
|
|
256
|
+
"music",
|
|
257
|
+
"apple music",
|
|
258
|
+
"spotify",
|
|
259
|
+
"tidal",
|
|
260
|
+
"youtube music",
|
|
261
|
+
"amazon music",
|
|
262
|
+
"deezer",
|
|
263
|
+
"qobuz",
|
|
264
|
+
"soundcloud",
|
|
265
|
+
"pandora",
|
|
266
|
+
"vox",
|
|
267
|
+
"audirvana",
|
|
268
|
+
"foobar2000",
|
|
269
|
+
"swinsian",
|
|
270
|
+
"cider"
|
|
271
|
+
]);
|
|
272
|
+
function isMusicApp(app, customMusicApps = []) {
|
|
273
|
+
if (app.isMusic) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
const nameLower = app.name.toLowerCase().trim();
|
|
277
|
+
const bundleLower = app.bundleId?.toLowerCase().trim();
|
|
278
|
+
if (bundleLower && KNOWN_MUSIC_BUNDLE_IDS.has(bundleLower)) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (KNOWN_MUSIC_APP_NAMES.has(nameLower)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (customMusicApps.length > 0) {
|
|
285
|
+
const customSet = new Set(
|
|
286
|
+
customMusicApps.map((a) => a.toLowerCase().trim())
|
|
287
|
+
);
|
|
288
|
+
if (customSet.has(nameLower) || bundleLower && customSet.has(bundleLower)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
173
294
|
function filterApps(apps, optionsOrExclude = {}) {
|
|
174
295
|
const opts = Array.isArray(optionsOrExclude) ? { exclude: optionsOrExclude } : optionsOrExclude;
|
|
175
296
|
const exclude = opts.exclude ?? [];
|
|
176
297
|
const includeFinder = opts.includeFinder ?? false;
|
|
177
|
-
const
|
|
298
|
+
const includeTrash = opts.includeTrash ?? false;
|
|
299
|
+
const groupBackground = opts.groupBackground ?? true;
|
|
300
|
+
const neverQuitMusic = opts.neverQuitMusic ?? false;
|
|
301
|
+
const customMusicApps = opts.musicApps ?? [];
|
|
178
302
|
const excludeSet = new Set(exclude.map((e) => e.toLowerCase().trim()));
|
|
179
|
-
const
|
|
303
|
+
const eligible = [];
|
|
180
304
|
for (const app of apps) {
|
|
181
305
|
const nameLower = app.name.toLowerCase().trim();
|
|
182
306
|
const bundleLower = app.bundleId?.toLowerCase().trim();
|
|
@@ -188,23 +312,73 @@ function filterApps(apps, optionsOrExclude = {}) {
|
|
|
188
312
|
continue;
|
|
189
313
|
}
|
|
190
314
|
}
|
|
191
|
-
if (
|
|
315
|
+
if (!includeTrash) {
|
|
316
|
+
if (nameLower === "trash" || bundleLower === "com.apple.trash") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (neverQuitMusic && isMusicApp(app, customMusicApps)) {
|
|
192
321
|
continue;
|
|
193
322
|
}
|
|
194
|
-
|
|
195
|
-
if (seen.has(key)) {
|
|
323
|
+
if (excludeSet.has(nameLower) || bundleLower && excludeSet.has(bundleLower)) {
|
|
196
324
|
continue;
|
|
197
325
|
}
|
|
198
|
-
|
|
199
|
-
|
|
326
|
+
eligible.push(app);
|
|
327
|
+
}
|
|
328
|
+
const result = [];
|
|
329
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const app of eligible) {
|
|
331
|
+
const nameLower = app.name.toLowerCase().trim();
|
|
332
|
+
const bundleLower = app.bundleId?.toLowerCase().trim();
|
|
333
|
+
const isBg = app.isBackground === true;
|
|
334
|
+
if (isBg && !groupBackground) {
|
|
335
|
+
result.push({ ...app });
|
|
336
|
+
} else {
|
|
337
|
+
let key = bundleLower ?? nameLower;
|
|
338
|
+
let cleanName = app.name;
|
|
339
|
+
if (isBg) {
|
|
340
|
+
const base = app.name.replace(/\s*\([^)]*\)$/, "").trim();
|
|
341
|
+
if (base) {
|
|
342
|
+
key = base.toLowerCase();
|
|
343
|
+
cleanName = base;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const existing = grouped.get(key);
|
|
347
|
+
if (existing) {
|
|
348
|
+
if (app.pid) {
|
|
349
|
+
if (!existing.pids) {
|
|
350
|
+
existing.pids = existing.pid ? [existing.pid] : [];
|
|
351
|
+
}
|
|
352
|
+
if (!existing.pids.includes(app.pid)) {
|
|
353
|
+
existing.pids.push(app.pid);
|
|
354
|
+
}
|
|
355
|
+
existing.count = existing.pids.length;
|
|
356
|
+
} else {
|
|
357
|
+
existing.count = (existing.count ?? 1) + 1;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
const item = { ...app, name: cleanName };
|
|
361
|
+
grouped.set(key, item);
|
|
362
|
+
result.push(item);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (includeTrash && !excludeSet.has("trash") && !excludeSet.has("com.apple.trash")) {
|
|
367
|
+
const hasTrash = result.some(
|
|
368
|
+
(a) => a.name.toLowerCase() === "trash" || a.bundleId?.toLowerCase() === "com.apple.trash"
|
|
369
|
+
);
|
|
370
|
+
if (!hasTrash) {
|
|
371
|
+
result.push({ name: "Trash", bundleId: "com.apple.trash" });
|
|
372
|
+
}
|
|
200
373
|
}
|
|
201
|
-
return sortApps(
|
|
374
|
+
return sortApps(result);
|
|
202
375
|
}
|
|
203
376
|
async function getRunningApps(optionsOrExclude, executor) {
|
|
204
377
|
let exec;
|
|
205
|
-
let filterOpts
|
|
378
|
+
let filterOpts;
|
|
206
379
|
let includeBackground = false;
|
|
207
380
|
if (typeof optionsOrExclude === "function") {
|
|
381
|
+
filterOpts = {};
|
|
208
382
|
exec = optionsOrExclude;
|
|
209
383
|
} else if (Array.isArray(optionsOrExclude)) {
|
|
210
384
|
filterOpts = { exclude: optionsOrExclude };
|
|
@@ -213,11 +387,25 @@ async function getRunningApps(optionsOrExclude, executor) {
|
|
|
213
387
|
const opts = optionsOrExclude;
|
|
214
388
|
filterOpts = {
|
|
215
389
|
exclude: opts.exclude ?? [],
|
|
216
|
-
includeFinder: opts.includeFinder ?? false
|
|
390
|
+
includeFinder: opts.includeFinder ?? false,
|
|
391
|
+
includeTrash: opts.includeTrash ?? false,
|
|
392
|
+
groupBackground: opts.groupBackground ?? true,
|
|
393
|
+
neverQuitMusic: opts.neverQuitMusic ?? false,
|
|
394
|
+
musicApps: opts.musicApps ?? []
|
|
217
395
|
};
|
|
218
396
|
includeBackground = opts.includeBackground ?? false;
|
|
219
397
|
exec = executor;
|
|
220
398
|
} else {
|
|
399
|
+
const config = loadConfig();
|
|
400
|
+
filterOpts = {
|
|
401
|
+
exclude: config.exclude,
|
|
402
|
+
includeFinder: config.includeFinder,
|
|
403
|
+
includeTrash: config.includeTrash,
|
|
404
|
+
groupBackground: config.groupBackground,
|
|
405
|
+
neverQuitMusic: config.neverQuitMusic,
|
|
406
|
+
musicApps: config.musicApps
|
|
407
|
+
};
|
|
408
|
+
includeBackground = config.includeBackground;
|
|
221
409
|
exec = executor;
|
|
222
410
|
}
|
|
223
411
|
const script = buildDiscoveryScript(includeBackground);
|
|
@@ -240,8 +428,19 @@ async function isAppRunning(target, executor) {
|
|
|
240
428
|
if (typeof target === "number") {
|
|
241
429
|
return isProcessAlive(target);
|
|
242
430
|
}
|
|
243
|
-
if (typeof target === "object"
|
|
244
|
-
|
|
431
|
+
if (typeof target === "object") {
|
|
432
|
+
if (target.bundleId === "com.apple.trash" || target.name.toLowerCase() === "trash") {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
if (target.pids && target.pids.length > 0) {
|
|
436
|
+
return target.pids.some((pid) => isProcessAlive(pid));
|
|
437
|
+
}
|
|
438
|
+
if (target.pid !== void 0 && target.pid > 0) {
|
|
439
|
+
return isProcessAlive(target.pid);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (typeof target === "string" && (target.toLowerCase() === "trash" || target.toLowerCase() === "com.apple.trash")) {
|
|
443
|
+
return false;
|
|
245
444
|
}
|
|
246
445
|
const name = typeof target === "string" ? target : target.name;
|
|
247
446
|
const bundleId = typeof target === "object" ? target.bundleId ?? "" : "";
|
|
@@ -353,13 +552,32 @@ function isCurrentTerminalApp(app, env = process.env) {
|
|
|
353
552
|
}
|
|
354
553
|
|
|
355
554
|
// src/macos/quit.ts
|
|
555
|
+
var EMPTY_TRASH_SCRIPT = `
|
|
556
|
+
ignoring application responses
|
|
557
|
+
tell application "Finder"
|
|
558
|
+
try
|
|
559
|
+
set warns before emptying to false
|
|
560
|
+
empty the trash
|
|
561
|
+
end try
|
|
562
|
+
end tell
|
|
563
|
+
end ignoring
|
|
564
|
+
`.trim();
|
|
356
565
|
var QUIT_JXA_SCRIPT = buildAppMatcherScript(`
|
|
357
566
|
const bundleId = argv[0] || '';
|
|
358
567
|
const name = argv[1] || '';
|
|
568
|
+
const pidStr = argv[2] || '';
|
|
569
|
+
const targetPid = pidStr ? parseInt(pidStr, 10) : 0;
|
|
359
570
|
const bid = app.bundleIdentifier ? app.bundleIdentifier.js : '';
|
|
360
571
|
const nm = app.localizedName ? app.localizedName.js : '';
|
|
361
|
-
|
|
362
|
-
|
|
572
|
+
const appPid = app.processIdentifier;
|
|
573
|
+
|
|
574
|
+
const matches = targetPid
|
|
575
|
+
? appPid === targetPid
|
|
576
|
+
: ((bundleId && bid === bundleId) || (name && nm.toLowerCase() === name.toLowerCase()));
|
|
577
|
+
|
|
578
|
+
if (matches) {
|
|
579
|
+
const res = typeof app.terminate === 'function' ? app.terminate() : app.terminate;
|
|
580
|
+
return String(res);
|
|
363
581
|
}
|
|
364
582
|
`);
|
|
365
583
|
function shellQuote(value) {
|
|
@@ -368,7 +586,9 @@ function shellQuote(value) {
|
|
|
368
586
|
function defaultDeferredQuitScheduler(app, force = false) {
|
|
369
587
|
try {
|
|
370
588
|
const bundleId = app.bundleId ?? "";
|
|
371
|
-
const
|
|
589
|
+
const pids = app.pids && app.pids.length > 0 ? app.pids : app.pid ? [app.pid] : [];
|
|
590
|
+
const pidArg = app.pid ? ` ${shellQuote(String(app.pid))}` : "";
|
|
591
|
+
const cmd = force && pids.length > 0 ? `sleep 0.4 && kill -9 ${pids.join(" ")}` : `sleep 0.4 && osascript -l JavaScript -e ${shellQuote(QUIT_JXA_SCRIPT)} ${shellQuote(bundleId)} ${shellQuote(app.name)}${pidArg}`;
|
|
372
592
|
const child = spawn("sh", ["-c", cmd], {
|
|
373
593
|
detached: true,
|
|
374
594
|
stdio: "ignore"
|
|
@@ -378,16 +598,48 @@ function defaultDeferredQuitScheduler(app, force = false) {
|
|
|
378
598
|
}
|
|
379
599
|
}
|
|
380
600
|
async function sendQuitSignal(app, executor) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
601
|
+
if (app.bundleId === "com.apple.trash" || app.name.toLowerCase() === "trash") {
|
|
602
|
+
await runAppleScript(EMPTY_TRASH_SCRIPT, executor);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (app.pids && app.pids.length > 1) {
|
|
606
|
+
let anyQuit = false;
|
|
607
|
+
for (const pid of app.pids) {
|
|
608
|
+
const args2 = [app.bundleId ?? "", app.name, String(pid)];
|
|
609
|
+
try {
|
|
610
|
+
const res2 = await runJXA(QUIT_JXA_SCRIPT, args2, executor);
|
|
611
|
+
if (res2.trim().toLowerCase() === "true") {
|
|
612
|
+
anyQuit = true;
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (!anyQuit) {
|
|
618
|
+
throw new Error(`Could not quit "${app.name}"`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const args = [app.bundleId ?? "", app.name];
|
|
623
|
+
if (app.pid && app.pid > 0) {
|
|
624
|
+
args.push(String(app.pid));
|
|
625
|
+
}
|
|
626
|
+
const res = await runJXA(QUIT_JXA_SCRIPT, args, executor);
|
|
386
627
|
if (res.trim().toLowerCase() !== "true") {
|
|
387
628
|
throw new Error(`Could not quit "${app.name}"`);
|
|
388
629
|
}
|
|
389
630
|
}
|
|
390
631
|
function forceQuitApp(app) {
|
|
632
|
+
if (app.pids && app.pids.length > 0) {
|
|
633
|
+
let anyKilled = false;
|
|
634
|
+
for (const pid of app.pids) {
|
|
635
|
+
try {
|
|
636
|
+
process.kill(pid, "SIGKILL");
|
|
637
|
+
anyKilled = true;
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return anyKilled;
|
|
642
|
+
}
|
|
391
643
|
if (app.pid && app.pid > 0) {
|
|
392
644
|
try {
|
|
393
645
|
process.kill(app.pid, "SIGKILL");
|
|
@@ -398,10 +650,18 @@ function forceQuitApp(app) {
|
|
|
398
650
|
}
|
|
399
651
|
return false;
|
|
400
652
|
}
|
|
653
|
+
var DEFAULT_QUIT_TIMEOUT_MS = 8e3;
|
|
401
654
|
async function quitApps(apps, options = {}, executor, deferredScheduler = defaultDeferredQuitScheduler) {
|
|
402
655
|
if (apps.length === 0) {
|
|
403
656
|
return [];
|
|
404
657
|
}
|
|
658
|
+
if (options.dryRun) {
|
|
659
|
+
return apps.map((app) => ({
|
|
660
|
+
app,
|
|
661
|
+
success: true,
|
|
662
|
+
forced: options.force ?? false
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
405
665
|
const currentTerminalApps = [];
|
|
406
666
|
const otherApps = [];
|
|
407
667
|
for (const app of apps) {
|
|
@@ -413,21 +673,33 @@ async function quitApps(apps, options = {}, executor, deferredScheduler = defaul
|
|
|
413
673
|
}
|
|
414
674
|
const results = [];
|
|
415
675
|
if (otherApps.length > 0) {
|
|
416
|
-
const
|
|
676
|
+
const maxTimeoutMs = options.timeoutMs ?? DEFAULT_QUIT_TIMEOUT_MS;
|
|
677
|
+
const pollIntervalMs = Math.min(150, maxTimeoutMs);
|
|
417
678
|
await Promise.allSettled(
|
|
418
679
|
otherApps.map((app) => sendQuitSignal(app, executor))
|
|
419
680
|
);
|
|
420
|
-
|
|
421
|
-
for (
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
681
|
+
const pending = /* @__PURE__ */ new Map();
|
|
682
|
+
for (let i = 0; i < otherApps.length; i++) {
|
|
683
|
+
const app = otherApps[i];
|
|
684
|
+
const key = app.pid ?? `idx_${i}_${app.name}`;
|
|
685
|
+
pending.set(key, app);
|
|
686
|
+
}
|
|
687
|
+
const start = Date.now();
|
|
688
|
+
while (pending.size > 0 && Date.now() - start <= maxTimeoutMs) {
|
|
689
|
+
await sleep(pollIntervalMs);
|
|
690
|
+
for (const [key, app] of Array.from(pending.entries())) {
|
|
691
|
+
const stillRunning = await isAppRunning(app, executor);
|
|
692
|
+
if (!stillRunning) {
|
|
693
|
+
results.push({ app, success: true, forced: false });
|
|
694
|
+
pending.delete(key);
|
|
695
|
+
}
|
|
426
696
|
}
|
|
697
|
+
}
|
|
698
|
+
for (const app of pending.values()) {
|
|
427
699
|
if (options.force) {
|
|
428
700
|
const killed = forceQuitApp(app);
|
|
429
701
|
if (killed) {
|
|
430
|
-
await sleep(
|
|
702
|
+
await sleep(150);
|
|
431
703
|
const aliveAfterForce = await isAppRunning(app, executor);
|
|
432
704
|
if (!aliveAfterForce) {
|
|
433
705
|
results.push({ app, success: true, forced: true });
|
|
@@ -469,14 +741,15 @@ function showOutro(message) {
|
|
|
469
741
|
function showCancel(message = "Cancelled.") {
|
|
470
742
|
cancel(message);
|
|
471
743
|
}
|
|
472
|
-
function renderResults(results) {
|
|
744
|
+
function renderResults(results, dryRun = false) {
|
|
473
745
|
for (const res of results) {
|
|
474
746
|
if (res.success) {
|
|
475
|
-
const label = res.forced ? `Force quit ${res.app.name}` : res.app.name;
|
|
747
|
+
const label = dryRun ? res.forced ? `[dry-run] Would force quit ${res.app.name}` : `[dry-run] Would quit ${res.app.name}` : res.forced ? `Force quit ${res.app.name}` : res.app.name;
|
|
476
748
|
log.success(label);
|
|
477
749
|
} else {
|
|
750
|
+
const prefix = dryRun ? "[dry-run] " : "";
|
|
478
751
|
const reason = res.error ? ` (${res.error})` : "";
|
|
479
|
-
log.error(
|
|
752
|
+
log.error(`${prefix}Could not quit ${res.app.name}${reason}`);
|
|
480
753
|
}
|
|
481
754
|
}
|
|
482
755
|
}
|
|
@@ -502,27 +775,37 @@ OPTIONS:
|
|
|
502
775
|
-y, --yes Skip confirmation prompts
|
|
503
776
|
-l, --list List running GUI apps and exit
|
|
504
777
|
-f, --force Force quit apps (SIGKILL) if normal quit fails
|
|
778
|
+
--dry-run Simulate quitting without terminating apps
|
|
505
779
|
-b, --background Include background processes in app list
|
|
506
780
|
--include-finder Include Finder in the app list
|
|
781
|
+
--include-trash Include Trash in the app list (empties on quit)
|
|
507
782
|
--exclude <apps> Add apps to persistent exclude list (comma-separated)
|
|
783
|
+
--check-update Check for package updates (60s rate limit)
|
|
784
|
+
--no-update-check Disable automatic update check for this run
|
|
508
785
|
--config Open interactive config manager
|
|
509
786
|
--json Output data in JSON format
|
|
510
787
|
-h, --help Show help information
|
|
511
788
|
-v, --version Show version number
|
|
512
789
|
|
|
513
790
|
COMMANDS:
|
|
514
|
-
config
|
|
791
|
+
config [action] Manage configuration (show, get, set, reset)
|
|
515
792
|
exclude Manage excluded applications interactively
|
|
793
|
+
check-update Check for package updates
|
|
516
794
|
|
|
517
795
|
EXAMPLES:
|
|
518
796
|
$ quitx Interactive app selector
|
|
519
797
|
$ quitx --all Quit all running apps (with confirmation)
|
|
798
|
+
$ quitx --all --dry-run Preview quitting all apps without killing them
|
|
520
799
|
$ quitx --all --yes Quit all running apps immediately
|
|
521
800
|
$ quitx --list List currently running GUI apps
|
|
522
801
|
$ quitx --list --background List all apps including background processes
|
|
523
802
|
$ quitx --list --include-finder List apps including Finder
|
|
524
803
|
$ quitx config Open interactive config manager
|
|
804
|
+
$ quitx config show Print current configuration
|
|
805
|
+
$ quitx config set force true Set default quit mode to force
|
|
806
|
+
$ quitx config get exclude Print excluded apps list
|
|
525
807
|
$ quitx exclude Open interactive menu to manage excluded apps
|
|
808
|
+
$ quitx check-update Check for package updates
|
|
526
809
|
$ quitx --exclude Spotify,Slack Add Spotify and Slack to exclude list
|
|
527
810
|
$ quitx Slack,Discord Quit specific apps directly
|
|
528
811
|
`.trim()
|
|
@@ -531,7 +814,7 @@ EXAMPLES:
|
|
|
531
814
|
function renderVersion(version) {
|
|
532
815
|
console.log(`quitx v${version}`);
|
|
533
816
|
}
|
|
534
|
-
var THANKS_MESSAGE = "\nThanks for using quitx
|
|
817
|
+
var THANKS_MESSAGE = "\nThanks for using quitx..!\nFor more visit - quitx.js.org";
|
|
535
818
|
function printThanks(options) {
|
|
536
819
|
if (options?.json || options?.quiet) return;
|
|
537
820
|
console.log(THANKS_MESSAGE);
|
|
@@ -541,125 +824,40 @@ var BACK_VALUE = "back";
|
|
|
541
824
|
function backOption() {
|
|
542
825
|
return { value: BACK_VALUE, label: "Back to main menu" };
|
|
543
826
|
}
|
|
544
|
-
function
|
|
545
|
-
if (currentValue === SELECT_ALL_VALUE) {
|
|
546
|
-
if (selectedValues.includes(SELECT_ALL_VALUE)) {
|
|
547
|
-
return [];
|
|
548
|
-
}
|
|
549
|
-
return [SELECT_ALL_VALUE];
|
|
550
|
-
}
|
|
551
|
-
if (selectedValues.includes(SELECT_ALL_VALUE)) {
|
|
552
|
-
return [...selectedValues];
|
|
553
|
-
}
|
|
554
|
-
const isSelected = selectedValues.includes(currentValue);
|
|
555
|
-
return isSelected ? selectedValues.filter((v) => v !== currentValue) : [...selectedValues, currentValue];
|
|
556
|
-
}
|
|
557
|
-
function attachQuitAllBehavior(prompt) {
|
|
558
|
-
let _cursor = prompt.cursor ?? 0;
|
|
559
|
-
let _value = prompt.value ? [...prompt.value] : [];
|
|
560
|
-
Object.defineProperty(prompt, "cursor", {
|
|
561
|
-
get() {
|
|
562
|
-
return _cursor;
|
|
563
|
-
},
|
|
564
|
-
set(val) {
|
|
565
|
-
if (_value.includes(SELECT_ALL_VALUE)) {
|
|
566
|
-
_cursor = 0;
|
|
567
|
-
} else {
|
|
568
|
-
_cursor = val;
|
|
569
|
-
}
|
|
570
|
-
},
|
|
571
|
-
configurable: true
|
|
572
|
-
});
|
|
573
|
-
Object.defineProperty(prompt, "value", {
|
|
574
|
-
get() {
|
|
575
|
-
return _value;
|
|
576
|
-
},
|
|
577
|
-
set(val) {
|
|
578
|
-
if (!Array.isArray(val)) {
|
|
579
|
-
_value = [];
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (val.includes(SELECT_ALL_VALUE)) {
|
|
583
|
-
_value = [SELECT_ALL_VALUE];
|
|
584
|
-
_cursor = 0;
|
|
585
|
-
} else {
|
|
586
|
-
_value = val.filter((v) => v !== SELECT_ALL_VALUE);
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
configurable: true
|
|
590
|
-
});
|
|
591
|
-
prompt.toggleValue = function() {
|
|
592
|
-
const cur = prompt.options[prompt.cursor ?? 0]?.value;
|
|
593
|
-
if (!cur) return;
|
|
594
|
-
prompt.value = handleQuitAllToggle(cur, prompt.value ?? []);
|
|
595
|
-
};
|
|
596
|
-
prompt.toggleAll = function() {
|
|
597
|
-
if (prompt.value?.includes(SELECT_ALL_VALUE)) {
|
|
598
|
-
prompt.value = [];
|
|
599
|
-
} else {
|
|
600
|
-
prompt.value = [SELECT_ALL_VALUE];
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
prompt.toggleInvert = function() {
|
|
604
|
-
if (prompt.value?.includes(SELECT_ALL_VALUE)) {
|
|
605
|
-
prompt.value = [];
|
|
606
|
-
} else {
|
|
607
|
-
prompt.value = [SELECT_ALL_VALUE];
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
async function selectApps(apps) {
|
|
827
|
+
async function selectApps(apps, options = {}) {
|
|
612
828
|
if (apps.length === 0) {
|
|
613
829
|
return [];
|
|
614
830
|
}
|
|
615
|
-
const
|
|
616
|
-
|
|
831
|
+
const bundleCounts = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const app of apps) {
|
|
833
|
+
const k = app.bundleId ?? app.name;
|
|
834
|
+
bundleCounts.set(k, (bundleCounts.get(k) ?? 0) + 1);
|
|
835
|
+
}
|
|
836
|
+
const appOptions = apps.map((app, index) => {
|
|
837
|
+
let hint = app.bundleId ?? (app.pid ? `PID: ${app.pid}` : void 0);
|
|
838
|
+
if (app.count && app.count > 1) {
|
|
839
|
+
hint = app.bundleId ? `${app.bundleId} (${app.count} instances)` : `${app.count} instances`;
|
|
840
|
+
} else if (app.pid && app.isBackground) {
|
|
841
|
+
hint = app.bundleId ? `${app.bundleId} (PID: ${app.pid})` : `PID: ${app.pid}`;
|
|
842
|
+
}
|
|
843
|
+
const base = app.bundleId ?? app.name;
|
|
844
|
+
const isDup = (bundleCounts.get(base) ?? 0) > 1;
|
|
845
|
+
const value = isDup && app.pid ? `${base}:${app.pid}` : app.bundleId ?? `${app.name}-${app.pid ?? index}`;
|
|
846
|
+
const label = app.count && app.count > 1 ? `${app.name} (${app.count})` : app.name;
|
|
617
847
|
return {
|
|
618
|
-
value
|
|
619
|
-
label
|
|
848
|
+
value,
|
|
849
|
+
label,
|
|
620
850
|
...hint ? { hint } : {}
|
|
621
851
|
};
|
|
622
852
|
});
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const initialValues = [SELECT_ALL_VALUE];
|
|
632
|
-
const proto = MultiSelectPrompt?.prototype;
|
|
633
|
-
const origPrompt = proto?.prompt;
|
|
634
|
-
const origToggleValue = proto?.toggleValue;
|
|
635
|
-
const origToggleAll = proto?.toggleAll;
|
|
636
|
-
const origToggleInvert = proto?.toggleInvert;
|
|
637
|
-
let selected;
|
|
638
|
-
try {
|
|
639
|
-
if (proto && origPrompt) {
|
|
640
|
-
proto.prompt = function() {
|
|
641
|
-
if (this.options?.some((o) => o.value === SELECT_ALL_VALUE)) {
|
|
642
|
-
attachQuitAllBehavior(
|
|
643
|
-
this
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
return origPrompt.call(this);
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
selected = await multiselect({
|
|
650
|
-
message: "Select apps to quit",
|
|
651
|
-
options,
|
|
652
|
-
required: false,
|
|
653
|
-
initialValues
|
|
654
|
-
});
|
|
655
|
-
} finally {
|
|
656
|
-
if (proto && origPrompt) proto.prompt = origPrompt;
|
|
657
|
-
if (proto && origToggleValue) proto.toggleValue = origToggleValue;
|
|
658
|
-
if (proto && origToggleAll) proto.toggleAll = origToggleAll;
|
|
659
|
-
if (proto && origToggleInvert) {
|
|
660
|
-
proto.toggleInvert = origToggleInvert;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
853
|
+
const defaultSelectAll = options.defaultSelectAll ?? true;
|
|
854
|
+
const initialValues = defaultSelectAll ? appOptions.map((opt) => opt.value) : [];
|
|
855
|
+
const selected = await multiselect({
|
|
856
|
+
message: "Select apps to quit",
|
|
857
|
+
options: appOptions,
|
|
858
|
+
required: false,
|
|
859
|
+
initialValues
|
|
860
|
+
});
|
|
663
861
|
if (isCancel(selected) || !Array.isArray(selected)) {
|
|
664
862
|
return selected;
|
|
665
863
|
}
|
|
@@ -671,9 +869,10 @@ async function selectApps(apps) {
|
|
|
671
869
|
return [...apps];
|
|
672
870
|
}
|
|
673
871
|
const selectedSet = new Set(selectedArray);
|
|
674
|
-
return apps.filter((app) => {
|
|
675
|
-
const
|
|
676
|
-
|
|
872
|
+
return apps.filter((app, index) => {
|
|
873
|
+
const optValue = appOptions[index]?.value;
|
|
874
|
+
const key = app.bundleId ?? `${app.name}-${app.pid ?? index}`;
|
|
875
|
+
return optValue && selectedSet.has(optValue) || app.bundleId && selectedSet.has(app.bundleId) || selectedSet.has(key);
|
|
677
876
|
});
|
|
678
877
|
}
|
|
679
878
|
async function shouldConfirmQuit(count, isAll = false, forceYes = false) {
|
|
@@ -699,11 +898,17 @@ async function allCommand(options = {}) {
|
|
|
699
898
|
const config = loadConfig();
|
|
700
899
|
const useForce = options.force ?? config.force === "force";
|
|
701
900
|
const includeFinder = options.includeFinder ?? config.includeFinder;
|
|
901
|
+
const includeTrash = options.includeTrash ?? config.includeTrash;
|
|
702
902
|
const includeBackground = options.includeBackground ?? config.includeBackground;
|
|
903
|
+
const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
|
|
703
904
|
const apps = await getRunningApps({
|
|
704
905
|
exclude: config.exclude,
|
|
705
906
|
includeFinder,
|
|
706
|
-
|
|
907
|
+
includeTrash,
|
|
908
|
+
includeBackground,
|
|
909
|
+
groupBackground: config.groupBackground,
|
|
910
|
+
neverQuitMusic,
|
|
911
|
+
musicApps: options.musicApps ?? config.musicApps
|
|
707
912
|
});
|
|
708
913
|
if (apps.length === 0) {
|
|
709
914
|
if (options.json) {
|
|
@@ -724,14 +929,20 @@ async function allCommand(options = {}) {
|
|
|
724
929
|
}
|
|
725
930
|
const s = spinner();
|
|
726
931
|
if (!options.json) {
|
|
727
|
-
s.start(
|
|
932
|
+
s.start(
|
|
933
|
+
options.dryRun ? `Simulating quit for ${apps.length} apps...` : `Quitting ${apps.length} apps...`
|
|
934
|
+
);
|
|
728
935
|
}
|
|
729
|
-
const results = await quitApps(apps, {
|
|
936
|
+
const results = await quitApps(apps, {
|
|
937
|
+
force: useForce,
|
|
938
|
+
dryRun: options.dryRun
|
|
939
|
+
});
|
|
730
940
|
if (!options.json) {
|
|
731
|
-
s.stop("Quitting complete");
|
|
732
|
-
renderResults(results);
|
|
941
|
+
s.stop(options.dryRun ? "Dry run complete" : "Quitting complete");
|
|
942
|
+
renderResults(results, options.dryRun);
|
|
733
943
|
const successCount = results.filter((r) => r.success).length;
|
|
734
|
-
|
|
944
|
+
const actionVerb = options.dryRun ? "Would quit" : "Quit";
|
|
945
|
+
showOutro(`${actionVerb} ${successCount} of ${apps.length} apps.`);
|
|
735
946
|
printThanks(options);
|
|
736
947
|
} else {
|
|
737
948
|
console.log(
|
|
@@ -798,6 +1009,37 @@ async function toggleIncludeFinder() {
|
|
|
798
1009
|
`Include Finder: ${config.includeFinder ? "enabled" : "disabled"}`
|
|
799
1010
|
);
|
|
800
1011
|
}
|
|
1012
|
+
async function toggleIncludeTrash() {
|
|
1013
|
+
const config = loadConfig();
|
|
1014
|
+
const choice = await select({
|
|
1015
|
+
message: `Include Empty Trash in list (current: ${config.includeTrash ? "enabled" : "disabled"})`,
|
|
1016
|
+
options: [
|
|
1017
|
+
{
|
|
1018
|
+
value: "disabled",
|
|
1019
|
+
label: "Disabled",
|
|
1020
|
+
hint: "Empty Trash is not shown in the app list."
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
value: "enabled",
|
|
1024
|
+
label: "Enabled",
|
|
1025
|
+
hint: "Shows 'Empty Trash' in list. Quitting it PERMANENTLY deletes all items in Trash (cannot be undone)."
|
|
1026
|
+
},
|
|
1027
|
+
backOption()
|
|
1028
|
+
],
|
|
1029
|
+
initialValue: config.includeTrash ? "enabled" : "disabled"
|
|
1030
|
+
});
|
|
1031
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1032
|
+
config.includeTrash = choice === "enabled";
|
|
1033
|
+
saveConfig(config);
|
|
1034
|
+
log.success(
|
|
1035
|
+
`Include Empty Trash in list: ${config.includeTrash ? "enabled" : "disabled"}`
|
|
1036
|
+
);
|
|
1037
|
+
if (config.includeTrash) {
|
|
1038
|
+
log.warn(
|
|
1039
|
+
"Note: Quitting 'Empty Trash' permanently and irreversibly deletes all files in macOS Trash."
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
801
1043
|
async function toggleIncludeBackground() {
|
|
802
1044
|
const config = loadConfig();
|
|
803
1045
|
const choice = await select({
|
|
@@ -824,6 +1066,111 @@ async function toggleIncludeBackground() {
|
|
|
824
1066
|
`Background apps: ${config.includeBackground ? "enabled" : "disabled"}`
|
|
825
1067
|
);
|
|
826
1068
|
}
|
|
1069
|
+
async function toggleDefaultSelection() {
|
|
1070
|
+
const config = loadConfig();
|
|
1071
|
+
const currentVal = config.defaultSelectAll ? "select-all" : "deselect-all";
|
|
1072
|
+
const choice = await select({
|
|
1073
|
+
message: `Default App Selection (current: ${currentVal})`,
|
|
1074
|
+
options: [
|
|
1075
|
+
{
|
|
1076
|
+
value: "select-all",
|
|
1077
|
+
label: "Select all apps",
|
|
1078
|
+
hint: "All apps are pre-selected by default when launching quitx."
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
value: "deselect-all",
|
|
1082
|
+
label: "Deselect all apps",
|
|
1083
|
+
hint: "No apps are selected by default. Pick specific apps to quit."
|
|
1084
|
+
},
|
|
1085
|
+
backOption()
|
|
1086
|
+
],
|
|
1087
|
+
initialValue: currentVal
|
|
1088
|
+
});
|
|
1089
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1090
|
+
config.defaultSelectAll = choice === "select-all";
|
|
1091
|
+
saveConfig(config);
|
|
1092
|
+
log.success(
|
|
1093
|
+
`Default selection: ${config.defaultSelectAll ? "select-all" : "deselect-all"}`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
async function toggleGroupBackground() {
|
|
1097
|
+
const config = loadConfig();
|
|
1098
|
+
const choice = await select({
|
|
1099
|
+
message: `Group Background Instances (current: ${config.groupBackground ? "enabled" : "disabled"})`,
|
|
1100
|
+
options: [
|
|
1101
|
+
{
|
|
1102
|
+
value: "enabled",
|
|
1103
|
+
label: "Enabled",
|
|
1104
|
+
hint: "Combines multiple instances of the same background app into a single item with instance count."
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
value: "disabled",
|
|
1108
|
+
label: "Disabled",
|
|
1109
|
+
hint: "Lists each background instance separately with its process ID (PID)."
|
|
1110
|
+
},
|
|
1111
|
+
backOption()
|
|
1112
|
+
],
|
|
1113
|
+
initialValue: config.groupBackground ? "enabled" : "disabled"
|
|
1114
|
+
});
|
|
1115
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1116
|
+
config.groupBackground = choice === "enabled";
|
|
1117
|
+
saveConfig(config);
|
|
1118
|
+
log.success(
|
|
1119
|
+
`Group background instances: ${config.groupBackground ? "enabled" : "disabled"}`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
async function toggleNeverQuitMusic() {
|
|
1123
|
+
const config = loadConfig();
|
|
1124
|
+
const choice = await select({
|
|
1125
|
+
message: `Never Quit Music Apps (current: ${config.neverQuitMusic ? "enabled" : "disabled"})`,
|
|
1126
|
+
options: [
|
|
1127
|
+
{
|
|
1128
|
+
value: "enabled",
|
|
1129
|
+
label: "Enabled",
|
|
1130
|
+
hint: "Never show or quit music players (Spotify, Apple Music, Tidal, etc.) to keep your audio playing."
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
value: "disabled",
|
|
1134
|
+
label: "Disabled",
|
|
1135
|
+
hint: "Include music apps in quit list normally."
|
|
1136
|
+
},
|
|
1137
|
+
backOption()
|
|
1138
|
+
],
|
|
1139
|
+
initialValue: config.neverQuitMusic ? "enabled" : "disabled"
|
|
1140
|
+
});
|
|
1141
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1142
|
+
config.neverQuitMusic = choice === "enabled";
|
|
1143
|
+
saveConfig(config);
|
|
1144
|
+
log.success(
|
|
1145
|
+
`Never quit music apps: ${config.neverQuitMusic ? "enabled" : "disabled"}`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
async function toggleAutoUpdate() {
|
|
1149
|
+
const config = loadConfig();
|
|
1150
|
+
const choice = await select({
|
|
1151
|
+
message: `Automatic Update Checks (current: ${config.autoUpdate ? "enabled" : "disabled"})`,
|
|
1152
|
+
options: [
|
|
1153
|
+
{
|
|
1154
|
+
value: "enabled",
|
|
1155
|
+
label: "Enabled",
|
|
1156
|
+
hint: "Check npm registry in background every 6 hours for updates."
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
value: "disabled",
|
|
1160
|
+
label: "Disabled",
|
|
1161
|
+
hint: "Never perform automatic background update checks."
|
|
1162
|
+
},
|
|
1163
|
+
backOption()
|
|
1164
|
+
],
|
|
1165
|
+
initialValue: config.autoUpdate ? "enabled" : "disabled"
|
|
1166
|
+
});
|
|
1167
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1168
|
+
config.autoUpdate = choice === "enabled";
|
|
1169
|
+
saveConfig(config);
|
|
1170
|
+
log.success(
|
|
1171
|
+
`Automatic update checks: ${config.autoUpdate ? "enabled" : "disabled"}`
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
827
1174
|
async function manageExcludedApps() {
|
|
828
1175
|
const choice = await select({
|
|
829
1176
|
message: "Manage excluded apps",
|
|
@@ -848,13 +1195,12 @@ ${config.exclude.map((e) => ` - ${e}`).join("\n")}`
|
|
|
848
1195
|
return;
|
|
849
1196
|
}
|
|
850
1197
|
if (choice === "add") {
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
config.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
);
|
|
1198
|
+
const available = await getRunningApps({
|
|
1199
|
+
exclude: config.exclude,
|
|
1200
|
+
includeFinder: config.includeFinder,
|
|
1201
|
+
includeBackground: config.includeBackground,
|
|
1202
|
+
groupBackground: config.groupBackground
|
|
1203
|
+
});
|
|
858
1204
|
if (available.length === 0) {
|
|
859
1205
|
log.info("All running apps are already excluded.");
|
|
860
1206
|
return;
|
|
@@ -899,8 +1245,88 @@ ${config.exclude.map((e) => ` - ${e}`).join("\n")}`
|
|
|
899
1245
|
log.success(`Removed ${selected.length} apps from exclude list.`);
|
|
900
1246
|
}
|
|
901
1247
|
}
|
|
1248
|
+
async function manageCustomMusicApps() {
|
|
1249
|
+
const choice = await select({
|
|
1250
|
+
message: "Manage custom music apps",
|
|
1251
|
+
options: [
|
|
1252
|
+
{ value: "add", label: "Add running apps to custom music list" },
|
|
1253
|
+
{ value: "remove", label: "Remove apps from custom music list" },
|
|
1254
|
+
{ value: "view", label: "View current custom music list" },
|
|
1255
|
+
backOption()
|
|
1256
|
+
]
|
|
1257
|
+
});
|
|
1258
|
+
if (isCancel(choice) || choice === BACK_VALUE) return;
|
|
1259
|
+
const config = loadConfig();
|
|
1260
|
+
if (choice === "view") {
|
|
1261
|
+
if (config.musicApps.length === 0) {
|
|
1262
|
+
log.info("No custom music apps registered.");
|
|
1263
|
+
} else {
|
|
1264
|
+
log.info(
|
|
1265
|
+
`Custom music apps:
|
|
1266
|
+
${config.musicApps.map((e) => ` - ${e}`).join("\n")}`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (choice === "add") {
|
|
1272
|
+
const available = await getRunningApps({
|
|
1273
|
+
exclude: config.exclude,
|
|
1274
|
+
includeFinder: config.includeFinder,
|
|
1275
|
+
includeBackground: config.includeBackground,
|
|
1276
|
+
groupBackground: config.groupBackground,
|
|
1277
|
+
neverQuitMusic: false
|
|
1278
|
+
});
|
|
1279
|
+
const unadded = available.filter(
|
|
1280
|
+
(a) => !config.musicApps.some(
|
|
1281
|
+
(m) => m.toLowerCase() === a.name.toLowerCase() || a.bundleId && m.toLowerCase() === a.bundleId.toLowerCase()
|
|
1282
|
+
)
|
|
1283
|
+
);
|
|
1284
|
+
if (unadded.length === 0) {
|
|
1285
|
+
log.info("All running apps are already in custom music list.");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const selected = await multiselect({
|
|
1289
|
+
message: "Select apps to mark as music players",
|
|
1290
|
+
options: unadded.map((a) => ({
|
|
1291
|
+
value: a.name,
|
|
1292
|
+
label: a.name,
|
|
1293
|
+
...a.bundleId ? { hint: a.bundleId } : {}
|
|
1294
|
+
})),
|
|
1295
|
+
required: false
|
|
1296
|
+
});
|
|
1297
|
+
if (isCancel(selected)) return;
|
|
1298
|
+
if (!Array.isArray(selected) || selected.length === 0) {
|
|
1299
|
+
log.info("No apps selected.");
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
addMusicApps(selected);
|
|
1303
|
+
log.success(`Added ${selected.length} apps to custom music list.`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (choice === "remove") {
|
|
1307
|
+
if (config.musicApps.length === 0) {
|
|
1308
|
+
log.info("No apps currently in custom music list.");
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const selected = await multiselect({
|
|
1312
|
+
message: "Select apps to remove from custom music list",
|
|
1313
|
+
options: config.musicApps.map((name) => ({
|
|
1314
|
+
value: name,
|
|
1315
|
+
label: name
|
|
1316
|
+
})),
|
|
1317
|
+
required: false
|
|
1318
|
+
});
|
|
1319
|
+
if (isCancel(selected)) return;
|
|
1320
|
+
if (!Array.isArray(selected) || selected.length === 0) {
|
|
1321
|
+
log.info("No apps selected.");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
removeMusicApps(selected);
|
|
1325
|
+
log.success(`Removed ${selected.length} apps from custom music list.`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
902
1328
|
function resetConfig() {
|
|
903
|
-
saveConfig({ ...DEFAULT_CONFIG, exclude: [] });
|
|
1329
|
+
saveConfig({ ...DEFAULT_CONFIG, exclude: [], musicApps: [] });
|
|
904
1330
|
log.success("Config reset to defaults.");
|
|
905
1331
|
}
|
|
906
1332
|
async function configCommand() {
|
|
@@ -913,28 +1339,58 @@ async function configCommand() {
|
|
|
913
1339
|
options: [
|
|
914
1340
|
{
|
|
915
1341
|
value: "quit-mode",
|
|
916
|
-
label:
|
|
917
|
-
hint: `current: ${config.force}`
|
|
1342
|
+
label: "Quit Mode",
|
|
1343
|
+
hint: `current: ${config.force === "normal" ? "Normal quit" : "Force quit"}`
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
value: "background",
|
|
1347
|
+
label: "View background apps",
|
|
1348
|
+
hint: `current: ${config.includeBackground ? "enabled" : "disabled"}`
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
value: "group-background",
|
|
1352
|
+
label: "Group background instances",
|
|
1353
|
+
hint: `current: ${config.groupBackground ? "enabled" : "disabled"}`
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
value: "never-quit-music",
|
|
1357
|
+
label: "Never quit music apps",
|
|
1358
|
+
hint: `current: ${config.neverQuitMusic ? "enabled" : "disabled"}`
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
value: "auto-update",
|
|
1362
|
+
label: "Automatic update checks",
|
|
1363
|
+
hint: `current: ${config.autoUpdate ? "enabled" : "disabled"}`
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
value: "default-select",
|
|
1367
|
+
label: "Deselect apps by default",
|
|
1368
|
+
hint: `current: ${!config.defaultSelectAll ? "enabled" : "disabled"}`
|
|
918
1369
|
},
|
|
919
1370
|
{
|
|
920
1371
|
value: "finder",
|
|
921
|
-
label:
|
|
1372
|
+
label: "Include Finder Windows in list",
|
|
922
1373
|
hint: `current: ${config.includeFinder ? "enabled" : "disabled"}`
|
|
923
1374
|
},
|
|
924
1375
|
{
|
|
925
|
-
value: "
|
|
926
|
-
label:
|
|
927
|
-
hint: `current: ${config.
|
|
1376
|
+
value: "trash",
|
|
1377
|
+
label: "Include Empty Trash in list",
|
|
1378
|
+
hint: `current: ${config.includeTrash ? "enabled (empties trash)" : "disabled"}`
|
|
928
1379
|
},
|
|
929
1380
|
{
|
|
930
1381
|
value: "exclude",
|
|
931
|
-
label: "Manage
|
|
1382
|
+
label: "Manage excluded apps",
|
|
932
1383
|
hint: `${config.exclude.length} excluded`
|
|
933
1384
|
},
|
|
1385
|
+
{
|
|
1386
|
+
value: "custom-music",
|
|
1387
|
+
label: "Manage custom music apps",
|
|
1388
|
+
hint: `${config.musicApps.length} custom apps`
|
|
1389
|
+
},
|
|
934
1390
|
{
|
|
935
1391
|
value: "reset",
|
|
936
|
-
label: "Reset
|
|
937
|
-
hint: "restore all settings"
|
|
1392
|
+
label: "Reset all",
|
|
1393
|
+
hint: "restore all settings to defaults"
|
|
938
1394
|
},
|
|
939
1395
|
{ value: "exit", label: "Exit" }
|
|
940
1396
|
]
|
|
@@ -944,15 +1400,155 @@ async function configCommand() {
|
|
|
944
1400
|
continue;
|
|
945
1401
|
}
|
|
946
1402
|
if (choice === "quit-mode") await toggleQuitMode();
|
|
947
|
-
if (choice === "finder") await toggleIncludeFinder();
|
|
948
1403
|
if (choice === "background") await toggleIncludeBackground();
|
|
1404
|
+
if (choice === "group-background") await toggleGroupBackground();
|
|
1405
|
+
if (choice === "never-quit-music") await toggleNeverQuitMusic();
|
|
1406
|
+
if (choice === "auto-update") await toggleAutoUpdate();
|
|
1407
|
+
if (choice === "default-select") await toggleDefaultSelection();
|
|
1408
|
+
if (choice === "finder") await toggleIncludeFinder();
|
|
1409
|
+
if (choice === "trash") await toggleIncludeTrash();
|
|
949
1410
|
if (choice === "exclude") await manageExcludedApps();
|
|
1411
|
+
if (choice === "custom-music") await manageCustomMusicApps();
|
|
950
1412
|
if (choice === "reset") resetConfig();
|
|
951
1413
|
}
|
|
952
1414
|
showOutro("Config saved.");
|
|
953
1415
|
printThanks();
|
|
954
1416
|
return 0;
|
|
955
1417
|
}
|
|
1418
|
+
var VALID_CONFIG_KEYS = [
|
|
1419
|
+
"exclude",
|
|
1420
|
+
"force",
|
|
1421
|
+
"includeFinder",
|
|
1422
|
+
"includeTrash",
|
|
1423
|
+
"includeBackground",
|
|
1424
|
+
"groupBackground",
|
|
1425
|
+
"defaultSelectAll",
|
|
1426
|
+
"neverQuitMusic",
|
|
1427
|
+
"musicApps",
|
|
1428
|
+
"autoUpdate"
|
|
1429
|
+
];
|
|
1430
|
+
async function handleConfigCli(options = {}) {
|
|
1431
|
+
const action = options.configAction;
|
|
1432
|
+
if (!action) {
|
|
1433
|
+
return await configCommand();
|
|
1434
|
+
}
|
|
1435
|
+
const config = loadConfig();
|
|
1436
|
+
if (action === "show") {
|
|
1437
|
+
if (options.json) {
|
|
1438
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1439
|
+
return 0;
|
|
1440
|
+
}
|
|
1441
|
+
console.log("quitx configuration:");
|
|
1442
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1443
|
+
const displayVal = Array.isArray(value) ? value.length === 0 ? "[]" : `[${value.join(", ")}]` : String(value);
|
|
1444
|
+
console.log(` ${key}: ${displayVal}`);
|
|
1445
|
+
}
|
|
1446
|
+
return 0;
|
|
1447
|
+
}
|
|
1448
|
+
if (action === "get") {
|
|
1449
|
+
if (!options.configKey) {
|
|
1450
|
+
console.error("\u2716 Missing config key. Usage: quitx config get <key>");
|
|
1451
|
+
return 1;
|
|
1452
|
+
}
|
|
1453
|
+
const key = options.configKey;
|
|
1454
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
1455
|
+
console.error(
|
|
1456
|
+
`\u2716 Unknown config key: "${options.configKey}". Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`
|
|
1457
|
+
);
|
|
1458
|
+
return 1;
|
|
1459
|
+
}
|
|
1460
|
+
const val = config[key];
|
|
1461
|
+
if (options.json) {
|
|
1462
|
+
console.log(JSON.stringify({ [key]: val }, null, 2));
|
|
1463
|
+
} else if (Array.isArray(val)) {
|
|
1464
|
+
console.log(val.join(", "));
|
|
1465
|
+
} else {
|
|
1466
|
+
console.log(String(val));
|
|
1467
|
+
}
|
|
1468
|
+
return 0;
|
|
1469
|
+
}
|
|
1470
|
+
if (action === "set") {
|
|
1471
|
+
if (!options.configKey) {
|
|
1472
|
+
console.error(
|
|
1473
|
+
"\u2716 Missing config key. Usage: quitx config set <key> <value>"
|
|
1474
|
+
);
|
|
1475
|
+
return 1;
|
|
1476
|
+
}
|
|
1477
|
+
if (options.configValue === void 0) {
|
|
1478
|
+
console.error(
|
|
1479
|
+
`\u2716 Missing value for "${options.configKey}". Usage: quitx config set <key> <value>`
|
|
1480
|
+
);
|
|
1481
|
+
return 1;
|
|
1482
|
+
}
|
|
1483
|
+
const key = options.configKey;
|
|
1484
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
1485
|
+
console.error(
|
|
1486
|
+
`\u2716 Unknown config key: "${options.configKey}". Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`
|
|
1487
|
+
);
|
|
1488
|
+
return 1;
|
|
1489
|
+
}
|
|
1490
|
+
const raw = options.configValue.trim();
|
|
1491
|
+
if (key === "force") {
|
|
1492
|
+
const lower = raw.toLowerCase();
|
|
1493
|
+
if (lower === "force" || lower === "true" || lower === "1") {
|
|
1494
|
+
config.force = "force";
|
|
1495
|
+
} else if (lower === "normal" || lower === "false" || lower === "0") {
|
|
1496
|
+
config.force = "normal";
|
|
1497
|
+
} else {
|
|
1498
|
+
console.error(
|
|
1499
|
+
`\u2716 Invalid value for force: "${raw}". Must be "normal" or "force".`
|
|
1500
|
+
);
|
|
1501
|
+
return 1;
|
|
1502
|
+
}
|
|
1503
|
+
} else if (key === "includeFinder" || key === "includeTrash" || key === "includeBackground" || key === "groupBackground" || key === "defaultSelectAll" || key === "neverQuitMusic" || key === "autoUpdate") {
|
|
1504
|
+
const lower = raw.toLowerCase();
|
|
1505
|
+
if (["true", "1", "yes", "on", "enable", "enabled"].includes(lower)) {
|
|
1506
|
+
config[key] = true;
|
|
1507
|
+
} else if (["false", "0", "no", "off", "disable", "disabled"].includes(lower)) {
|
|
1508
|
+
config[key] = false;
|
|
1509
|
+
} else {
|
|
1510
|
+
console.error(
|
|
1511
|
+
`\u2716 Invalid boolean for ${key}: "${raw}". Use true or false.`
|
|
1512
|
+
);
|
|
1513
|
+
return 1;
|
|
1514
|
+
}
|
|
1515
|
+
} else if (key === "exclude" || key === "musicApps") {
|
|
1516
|
+
const items = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1517
|
+
config[key] = items;
|
|
1518
|
+
}
|
|
1519
|
+
saveConfig(config);
|
|
1520
|
+
if (options.json) {
|
|
1521
|
+
console.log(JSON.stringify({ [key]: config[key] }, null, 2));
|
|
1522
|
+
} else {
|
|
1523
|
+
const displayVal = Array.isArray(config[key]) ? `[${config[key].join(", ")}]` : String(config[key]);
|
|
1524
|
+
console.log(`Set ${key} = ${displayVal}`);
|
|
1525
|
+
}
|
|
1526
|
+
return 0;
|
|
1527
|
+
}
|
|
1528
|
+
if (action === "reset") {
|
|
1529
|
+
if (!options.yes) {
|
|
1530
|
+
const confirm2 = await select({
|
|
1531
|
+
message: "Reset all settings to defaults?",
|
|
1532
|
+
options: [
|
|
1533
|
+
{ value: "yes", label: "Yes, reset all" },
|
|
1534
|
+
{ value: "no", label: "No, keep current settings" }
|
|
1535
|
+
],
|
|
1536
|
+
initialValue: "no"
|
|
1537
|
+
});
|
|
1538
|
+
if (isCancel(confirm2) || confirm2 !== "yes") {
|
|
1539
|
+
return 0;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
saveConfig({ ...DEFAULT_CONFIG, exclude: [], musicApps: [] });
|
|
1543
|
+
if (options.json) {
|
|
1544
|
+
console.log(JSON.stringify({ reset: true }, null, 2));
|
|
1545
|
+
} else {
|
|
1546
|
+
log.success("Config reset to defaults.");
|
|
1547
|
+
}
|
|
1548
|
+
return 0;
|
|
1549
|
+
}
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
956
1552
|
async function manageExcludeInteractive() {
|
|
957
1553
|
showIntro();
|
|
958
1554
|
const choice = await select({
|
|
@@ -982,13 +1578,12 @@ ${currentConfig.exclude.map((e) => ` - ${e}`).join("\n")}`
|
|
|
982
1578
|
return 0;
|
|
983
1579
|
}
|
|
984
1580
|
if (choice === "add") {
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
currentConfig.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
);
|
|
1581
|
+
const available = await getRunningApps({
|
|
1582
|
+
exclude: currentConfig.exclude,
|
|
1583
|
+
includeFinder: currentConfig.includeFinder,
|
|
1584
|
+
includeBackground: currentConfig.includeBackground,
|
|
1585
|
+
groupBackground: currentConfig.groupBackground
|
|
1586
|
+
});
|
|
992
1587
|
if (available.length === 0) {
|
|
993
1588
|
showOutro("All running apps are already excluded.");
|
|
994
1589
|
printThanks();
|
|
@@ -1077,11 +1672,17 @@ async function interactiveCommand(options = {}) {
|
|
|
1077
1672
|
const config = loadConfig();
|
|
1078
1673
|
const useForce = options.force ?? config.force === "force";
|
|
1079
1674
|
const includeFinder = options.includeFinder ?? config.includeFinder;
|
|
1675
|
+
const includeTrash = options.includeTrash ?? config.includeTrash;
|
|
1080
1676
|
const includeBackground = options.includeBackground ?? config.includeBackground;
|
|
1677
|
+
const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
|
|
1081
1678
|
const apps = await getRunningApps({
|
|
1082
1679
|
exclude: config.exclude,
|
|
1083
1680
|
includeFinder,
|
|
1084
|
-
|
|
1681
|
+
includeTrash,
|
|
1682
|
+
includeBackground,
|
|
1683
|
+
groupBackground: config.groupBackground,
|
|
1684
|
+
neverQuitMusic,
|
|
1685
|
+
musicApps: options.musicApps ?? config.musicApps
|
|
1085
1686
|
});
|
|
1086
1687
|
if (!options.json) {
|
|
1087
1688
|
s.stop(
|
|
@@ -1120,7 +1721,9 @@ async function interactiveCommand(options = {}) {
|
|
|
1120
1721
|
return 0;
|
|
1121
1722
|
}
|
|
1122
1723
|
} else {
|
|
1123
|
-
const selected = await selectApps(apps
|
|
1724
|
+
const selected = await selectApps(apps, {
|
|
1725
|
+
defaultSelectAll: config.defaultSelectAll
|
|
1726
|
+
});
|
|
1124
1727
|
if (typeof selected === "symbol" || isCancel(selected)) {
|
|
1125
1728
|
showCancel("Cancelled.");
|
|
1126
1729
|
printThanks(options);
|
|
@@ -1134,11 +1737,7 @@ async function interactiveCommand(options = {}) {
|
|
|
1134
1737
|
targetApps = selected;
|
|
1135
1738
|
}
|
|
1136
1739
|
if (!options.yes) {
|
|
1137
|
-
const confirmation = await shouldConfirmQuit(
|
|
1138
|
-
targetApps.length,
|
|
1139
|
-
targetApps.length === apps.length,
|
|
1140
|
-
false
|
|
1141
|
-
);
|
|
1740
|
+
const confirmation = await shouldConfirmQuit(targetApps.length, false);
|
|
1142
1741
|
if (isCancel(confirmation) || confirmation !== true) {
|
|
1143
1742
|
showCancel("Cancelled.");
|
|
1144
1743
|
printThanks(options);
|
|
@@ -1147,15 +1746,21 @@ async function interactiveCommand(options = {}) {
|
|
|
1147
1746
|
}
|
|
1148
1747
|
if (!options.json) {
|
|
1149
1748
|
s.start(
|
|
1150
|
-
`Quitting ${targetApps.length} ${targetApps.length === 1 ? "app" : "apps"}...`
|
|
1749
|
+
options.dryRun ? `Simulating quit for ${targetApps.length} ${targetApps.length === 1 ? "app" : "apps"}...` : `Quitting ${targetApps.length} ${targetApps.length === 1 ? "app" : "apps"}...`
|
|
1151
1750
|
);
|
|
1152
1751
|
}
|
|
1153
|
-
const results = await quitApps(targetApps, {
|
|
1752
|
+
const results = await quitApps(targetApps, {
|
|
1753
|
+
force: useForce,
|
|
1754
|
+
dryRun: options.dryRun
|
|
1755
|
+
});
|
|
1154
1756
|
if (!options.json) {
|
|
1155
|
-
s.stop("Quitting complete");
|
|
1156
|
-
renderResults(results);
|
|
1757
|
+
s.stop(options.dryRun ? "Dry run complete" : "Quitting complete");
|
|
1758
|
+
renderResults(results, options.dryRun);
|
|
1157
1759
|
const successCount = results.filter((r) => r.success).length;
|
|
1158
|
-
|
|
1760
|
+
const actionVerb = options.dryRun ? "Would quit" : "Quit";
|
|
1761
|
+
showOutro(
|
|
1762
|
+
`Done. ${actionVerb} ${successCount} of ${targetApps.length} apps.`
|
|
1763
|
+
);
|
|
1159
1764
|
printThanks(options);
|
|
1160
1765
|
} else {
|
|
1161
1766
|
console.log(
|
|
@@ -1177,15 +1782,214 @@ async function interactiveCommand(options = {}) {
|
|
|
1177
1782
|
async function listCommand(options = {}) {
|
|
1178
1783
|
const config = loadConfig();
|
|
1179
1784
|
const includeFinder = options.includeFinder ?? config.includeFinder;
|
|
1785
|
+
const includeTrash = options.includeTrash ?? config.includeTrash;
|
|
1180
1786
|
const includeBackground = options.includeBackground ?? config.includeBackground;
|
|
1787
|
+
const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
|
|
1181
1788
|
const apps = await getRunningApps({
|
|
1182
1789
|
exclude: config.exclude,
|
|
1183
1790
|
includeFinder,
|
|
1184
|
-
|
|
1791
|
+
includeTrash,
|
|
1792
|
+
includeBackground,
|
|
1793
|
+
groupBackground: config.groupBackground,
|
|
1794
|
+
neverQuitMusic,
|
|
1795
|
+
musicApps: options.musicApps ?? config.musicApps
|
|
1185
1796
|
});
|
|
1186
1797
|
renderList(apps, options.json);
|
|
1187
1798
|
return 0;
|
|
1188
1799
|
}
|
|
1800
|
+
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
1801
|
+
var MANUAL_RATE_LIMIT_MS = 60 * 1e3;
|
|
1802
|
+
var REQUEST_TIMEOUT_MS = 1500;
|
|
1803
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@coreify%2Fquitx/latest";
|
|
1804
|
+
var FALLBACK_REGISTRY_URL = "https://registry.npmjs.org/quitx/latest";
|
|
1805
|
+
var RELEASE_URL = "https://github.com/coreify/quitx/releases/latest";
|
|
1806
|
+
function updateCacheDirectory() {
|
|
1807
|
+
const configured = process.env.XDG_CACHE_HOME;
|
|
1808
|
+
if (configured) return join(configured, "quitx");
|
|
1809
|
+
return join(homedir(), ".quitx");
|
|
1810
|
+
}
|
|
1811
|
+
function parseVersion(version) {
|
|
1812
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z.-]+))?(?:\+[\dA-Za-z.-]+)?$/u.exec(
|
|
1813
|
+
version.trim()
|
|
1814
|
+
);
|
|
1815
|
+
if (!match) return void 0;
|
|
1816
|
+
const numbers = [
|
|
1817
|
+
Number(match[1]),
|
|
1818
|
+
Number(match[2]),
|
|
1819
|
+
Number(match[3])
|
|
1820
|
+
];
|
|
1821
|
+
return match[4] ? { numbers, prerelease: match[4].split(".") } : { numbers };
|
|
1822
|
+
}
|
|
1823
|
+
function compareVersions(left, right) {
|
|
1824
|
+
const leftVersion = parseVersion(left);
|
|
1825
|
+
const rightVersion = parseVersion(right);
|
|
1826
|
+
if (!leftVersion || !rightVersion) return 0;
|
|
1827
|
+
for (let index = 0; index < 3; index++) {
|
|
1828
|
+
const difference = leftVersion.numbers[index] - rightVersion.numbers[index];
|
|
1829
|
+
if (difference !== 0) return Math.sign(difference);
|
|
1830
|
+
}
|
|
1831
|
+
if (!leftVersion.prerelease && !rightVersion.prerelease) return 0;
|
|
1832
|
+
if (!leftVersion.prerelease) return 1;
|
|
1833
|
+
if (!rightVersion.prerelease) return -1;
|
|
1834
|
+
const length = Math.max(
|
|
1835
|
+
leftVersion.prerelease.length,
|
|
1836
|
+
rightVersion.prerelease.length
|
|
1837
|
+
);
|
|
1838
|
+
for (let index = 0; index < length; index++) {
|
|
1839
|
+
const leftPart = leftVersion.prerelease[index];
|
|
1840
|
+
const rightPart = rightVersion.prerelease[index];
|
|
1841
|
+
if (leftPart === void 0) return -1;
|
|
1842
|
+
if (rightPart === void 0) return 1;
|
|
1843
|
+
if (leftPart === rightPart) continue;
|
|
1844
|
+
const leftNumber = /^\d+$/u.test(leftPart) ? Number(leftPart) : void 0;
|
|
1845
|
+
const rightNumber = /^\d+$/u.test(rightPart) ? Number(rightPart) : void 0;
|
|
1846
|
+
if (leftNumber !== void 0 && rightNumber !== void 0) {
|
|
1847
|
+
return Math.sign(leftNumber - rightNumber);
|
|
1848
|
+
}
|
|
1849
|
+
if (leftNumber !== void 0) return -1;
|
|
1850
|
+
if (rightNumber !== void 0) return 1;
|
|
1851
|
+
return leftPart.localeCompare(rightPart);
|
|
1852
|
+
}
|
|
1853
|
+
return 0;
|
|
1854
|
+
}
|
|
1855
|
+
async function readCache(path) {
|
|
1856
|
+
try {
|
|
1857
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
1858
|
+
if (typeof value !== "object" || value === null) return {};
|
|
1859
|
+
const record = value;
|
|
1860
|
+
return {
|
|
1861
|
+
...typeof record.checkedAt === "number" && Number.isFinite(record.checkedAt) ? { checkedAt: record.checkedAt } : {},
|
|
1862
|
+
...typeof record.ignoredVersion === "string" && parseVersion(record.ignoredVersion) ? { ignoredVersion: record.ignoredVersion } : {},
|
|
1863
|
+
...typeof record.latestVersion === "string" && parseVersion(record.latestVersion) ? { latestVersion: record.latestVersion } : {}
|
|
1864
|
+
};
|
|
1865
|
+
} catch {
|
|
1866
|
+
return {};
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function writeCache(path, value) {
|
|
1870
|
+
try {
|
|
1871
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1872
|
+
await writeFile(path, JSON.stringify(value), { mode: 384 });
|
|
1873
|
+
} catch {
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function fetchLatestVersion(fetcher = fetch) {
|
|
1877
|
+
const urls = [REGISTRY_URL, FALLBACK_REGISTRY_URL];
|
|
1878
|
+
for (const url of urls) {
|
|
1879
|
+
const controller = new AbortController();
|
|
1880
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1881
|
+
timer.unref();
|
|
1882
|
+
try {
|
|
1883
|
+
const response = await fetcher(url, {
|
|
1884
|
+
headers: { accept: "application/json" },
|
|
1885
|
+
signal: controller.signal
|
|
1886
|
+
});
|
|
1887
|
+
if (!response.ok) continue;
|
|
1888
|
+
const value = await response.json();
|
|
1889
|
+
if (typeof value === "object" && value !== null && "version" in value && typeof value.version === "string" && parseVersion(value.version)) {
|
|
1890
|
+
return value.version;
|
|
1891
|
+
}
|
|
1892
|
+
} catch {
|
|
1893
|
+
} finally {
|
|
1894
|
+
clearTimeout(timer);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
throw new Error("Unable to fetch latest version from npm registry");
|
|
1898
|
+
}
|
|
1899
|
+
async function checkForUpdate(currentVersion, options = {}) {
|
|
1900
|
+
if (!parseVersion(currentVersion)) return void 0;
|
|
1901
|
+
const now = options.now ?? Date.now();
|
|
1902
|
+
const cachePath = join(
|
|
1903
|
+
options.cacheDirectory ?? updateCacheDirectory(),
|
|
1904
|
+
"update.json"
|
|
1905
|
+
);
|
|
1906
|
+
const cache = await readCache(cachePath);
|
|
1907
|
+
let latestVersion = cache.latestVersion;
|
|
1908
|
+
if (options.force || !cache.checkedAt || now - cache.checkedAt >= CHECK_INTERVAL_MS) {
|
|
1909
|
+
try {
|
|
1910
|
+
latestVersion = await fetchLatestVersion(options.fetcher ?? fetch);
|
|
1911
|
+
await writeCache(cachePath, {
|
|
1912
|
+
...cache,
|
|
1913
|
+
checkedAt: now,
|
|
1914
|
+
latestVersion
|
|
1915
|
+
});
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (!latestVersion || latestVersion === cache.ignoredVersion || compareVersions(latestVersion, currentVersion) <= 0) {
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
currentVersion,
|
|
1924
|
+
latestVersion,
|
|
1925
|
+
releaseUrl: RELEASE_URL
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
async function checkUpdateManually(currentVersion, options = {}) {
|
|
1929
|
+
const now = options.now ?? Date.now();
|
|
1930
|
+
const cachePath = join(
|
|
1931
|
+
options.cacheDirectory ?? updateCacheDirectory(),
|
|
1932
|
+
"update.json"
|
|
1933
|
+
);
|
|
1934
|
+
const cache = await readCache(cachePath);
|
|
1935
|
+
let latestVersion = cache.latestVersion;
|
|
1936
|
+
const isRateLimited = Boolean(
|
|
1937
|
+
!options.force && cache.checkedAt && now - cache.checkedAt < MANUAL_RATE_LIMIT_MS
|
|
1938
|
+
);
|
|
1939
|
+
if (!isRateLimited) {
|
|
1940
|
+
try {
|
|
1941
|
+
latestVersion = await fetchLatestVersion(options.fetcher ?? fetch);
|
|
1942
|
+
await writeCache(cachePath, {
|
|
1943
|
+
...cache,
|
|
1944
|
+
checkedAt: now,
|
|
1945
|
+
latestVersion
|
|
1946
|
+
});
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const effectiveLatest = latestVersion ?? currentVersion;
|
|
1951
|
+
const updateAvailable = Boolean(
|
|
1952
|
+
latestVersion && compareVersions(latestVersion, currentVersion) > 0
|
|
1953
|
+
);
|
|
1954
|
+
return {
|
|
1955
|
+
currentVersion,
|
|
1956
|
+
latestVersion: effectiveLatest,
|
|
1957
|
+
rateLimited: isRateLimited,
|
|
1958
|
+
releaseUrl: RELEASE_URL,
|
|
1959
|
+
updateAvailable
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
async function ignoreUpdateVersion(version, cacheDir = updateCacheDirectory()) {
|
|
1963
|
+
const cachePath = join(cacheDir, "update.json");
|
|
1964
|
+
const cache = await readCache(cachePath);
|
|
1965
|
+
await writeCache(cachePath, { ...cache, ignoredVersion: version });
|
|
1966
|
+
}
|
|
1967
|
+
function installUpdate(version, spawner = spawn) {
|
|
1968
|
+
if (!parseVersion(version)) {
|
|
1969
|
+
return Promise.reject(new Error(`Invalid update version: ${version}`));
|
|
1970
|
+
}
|
|
1971
|
+
return new Promise((resolve, reject) => {
|
|
1972
|
+
const pkg = `@coreify/quitx@${version}`;
|
|
1973
|
+
const child = spawner("npm", ["install", "--global", pkg], {
|
|
1974
|
+
stdio: "inherit",
|
|
1975
|
+
windowsHide: true
|
|
1976
|
+
});
|
|
1977
|
+
child.once("error", reject);
|
|
1978
|
+
child.once("exit", (code, signal) => {
|
|
1979
|
+
if (code === 0) resolve();
|
|
1980
|
+
else {
|
|
1981
|
+
reject(
|
|
1982
|
+
new Error(
|
|
1983
|
+
signal ? `Update stopped by ${signal}` : `npm install exited with code ${code ?? "unknown"}`
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
function updateNotice(update) {
|
|
1991
|
+
return `Update available: ${update.currentVersion} \u2192 ${update.latestVersion}. Run: npm install --global @coreify/quitx@latest`;
|
|
1992
|
+
}
|
|
1189
1993
|
|
|
1190
1994
|
// src/utils/platform.ts
|
|
1191
1995
|
function isMacOS(platform = process.platform) {
|
|
@@ -1223,10 +2027,42 @@ function parseCliArgs(args) {
|
|
|
1223
2027
|
for (let i = 0; i < args.length; i++) {
|
|
1224
2028
|
const arg = args[i];
|
|
1225
2029
|
if (!arg) continue;
|
|
1226
|
-
if (arg === "
|
|
2030
|
+
if (arg === "--dry-run") {
|
|
2031
|
+
options.dryRun = true;
|
|
2032
|
+
} else if (arg === "config") {
|
|
1227
2033
|
options.manageConfig = true;
|
|
2034
|
+
const next = args[i + 1];
|
|
2035
|
+
if (next && !next.startsWith("-") && ["show", "get", "set", "reset"].includes(next.toLowerCase())) {
|
|
2036
|
+
options.configAction = next.toLowerCase();
|
|
2037
|
+
i++;
|
|
2038
|
+
if (options.configAction === "get") {
|
|
2039
|
+
const key = args[++i];
|
|
2040
|
+
if (key && !key.startsWith("-")) {
|
|
2041
|
+
options.configKey = key;
|
|
2042
|
+
} else if (key) {
|
|
2043
|
+
i--;
|
|
2044
|
+
}
|
|
2045
|
+
} else if (options.configAction === "set") {
|
|
2046
|
+
const key = args[++i];
|
|
2047
|
+
if (key && !key.startsWith("-")) {
|
|
2048
|
+
options.configKey = key;
|
|
2049
|
+
const val = args[++i];
|
|
2050
|
+
if (val && !val.startsWith("-")) {
|
|
2051
|
+
options.configValue = val;
|
|
2052
|
+
} else if (val) {
|
|
2053
|
+
i--;
|
|
2054
|
+
}
|
|
2055
|
+
} else if (key) {
|
|
2056
|
+
i--;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
1228
2060
|
} else if (arg === "exclude") {
|
|
1229
2061
|
options.manageExclude = true;
|
|
2062
|
+
} else if (arg === "check-update" || arg === "--check-update") {
|
|
2063
|
+
options.checkUpdate = true;
|
|
2064
|
+
} else if (arg === "--no-update-check") {
|
|
2065
|
+
options.noUpdateCheck = true;
|
|
1230
2066
|
} else if (arg === "-a" || arg === "--all") {
|
|
1231
2067
|
options.all = true;
|
|
1232
2068
|
} else if (arg === "-y" || arg === "--yes") {
|
|
@@ -1243,8 +2079,12 @@ function parseCliArgs(args) {
|
|
|
1243
2079
|
options.json = true;
|
|
1244
2080
|
} else if (arg === "--include-finder") {
|
|
1245
2081
|
options.includeFinder = true;
|
|
2082
|
+
} else if (arg === "--include-trash") {
|
|
2083
|
+
options.includeTrash = true;
|
|
1246
2084
|
} else if (arg === "-b" || arg === "--background") {
|
|
1247
2085
|
options.includeBackground = true;
|
|
2086
|
+
} else if (arg === "--never-quit-music") {
|
|
2087
|
+
options.neverQuitMusic = true;
|
|
1248
2088
|
} else if (arg === "--config") {
|
|
1249
2089
|
options.manageConfig = true;
|
|
1250
2090
|
} else if (arg === "--exclude") {
|
|
@@ -1266,7 +2106,13 @@ function parseCliArgs(args) {
|
|
|
1266
2106
|
}
|
|
1267
2107
|
}
|
|
1268
2108
|
if (rawPositional.length > 0) {
|
|
1269
|
-
if (options.
|
|
2109
|
+
if (options.manageConfig) {
|
|
2110
|
+
if (!options.configAction) {
|
|
2111
|
+
throw new Error(
|
|
2112
|
+
`Unknown config command: "${rawPositional[0]}". Usage: quitx config [show|get|set|reset]`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
} else if (options.manageExclude) {
|
|
1270
2116
|
if (rawPositional.length > 1 && !rawPositional.some((a) => a.includes(","))) {
|
|
1271
2117
|
throw new Error(
|
|
1272
2118
|
"Multiple exclude applications must be comma-separated, not space-separated. Example: quitx exclude Spotify,Discord"
|
|
@@ -1298,9 +2144,135 @@ function parseCliArgs(args) {
|
|
|
1298
2144
|
}
|
|
1299
2145
|
return options;
|
|
1300
2146
|
}
|
|
2147
|
+
async function handleManualUpdateCheck(version, options) {
|
|
2148
|
+
const isInteractive = !options.json && Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
2149
|
+
if (options.json) {
|
|
2150
|
+
const result2 = await checkUpdateManually(version, { force: options.force });
|
|
2151
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
2152
|
+
return 0;
|
|
2153
|
+
}
|
|
2154
|
+
if (isInteractive) {
|
|
2155
|
+
showIntro();
|
|
2156
|
+
const checkSpinner = spinner();
|
|
2157
|
+
checkSpinner.start("Checking npm registry for updates");
|
|
2158
|
+
const result2 = await checkUpdateManually(version, { force: options.force });
|
|
2159
|
+
if (result2.rateLimited) {
|
|
2160
|
+
checkSpinner.stop(
|
|
2161
|
+
`Checked recently (rate limited, 60s cooldown). Latest: v${result2.latestVersion}`
|
|
2162
|
+
);
|
|
2163
|
+
} else if (result2.updateAvailable) {
|
|
2164
|
+
checkSpinner.stop(
|
|
2165
|
+
`Update available: ${result2.currentVersion} \u2192 ${result2.latestVersion}`
|
|
2166
|
+
);
|
|
2167
|
+
} else {
|
|
2168
|
+
checkSpinner.stop(`quitx is up to date (v${result2.currentVersion})`);
|
|
2169
|
+
}
|
|
2170
|
+
if (result2.updateAvailable) {
|
|
2171
|
+
console.log(
|
|
2172
|
+
"\n Upgrade Available: Run npm install --global @coreify/quitx@latest\n"
|
|
2173
|
+
);
|
|
2174
|
+
const answer = await select({
|
|
2175
|
+
message: `Install v${result2.latestVersion} now?`,
|
|
2176
|
+
options: [
|
|
2177
|
+
{ value: "install", label: "Install update now" },
|
|
2178
|
+
{ value: "skip", label: "Skip for now" }
|
|
2179
|
+
],
|
|
2180
|
+
initialValue: "install"
|
|
2181
|
+
});
|
|
2182
|
+
if (isCancel(answer) || answer === "skip") {
|
|
2183
|
+
printThanks(options);
|
|
2184
|
+
return 0;
|
|
2185
|
+
}
|
|
2186
|
+
if (answer === "install") {
|
|
2187
|
+
console.log(`Updating to ${result2.latestVersion}...`);
|
|
2188
|
+
try {
|
|
2189
|
+
await installUpdate(result2.latestVersion);
|
|
2190
|
+
showOutro(
|
|
2191
|
+
`Updated to ${result2.latestVersion}. Restart quitx to use it.`
|
|
2192
|
+
);
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2195
|
+
console.error(`Failed to install update: ${msg}`);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
printThanks(options);
|
|
2200
|
+
return 0;
|
|
2201
|
+
}
|
|
2202
|
+
const result = await checkUpdateManually(version, { force: options.force });
|
|
2203
|
+
if (result.updateAvailable) {
|
|
2204
|
+
console.log(
|
|
2205
|
+
`Update available: ${result.currentVersion} \u2192 ${result.latestVersion}`
|
|
2206
|
+
);
|
|
2207
|
+
console.log("Run: npm install --global @coreify/quitx@latest");
|
|
2208
|
+
} else {
|
|
2209
|
+
console.log(`quitx is up to date (${result.currentVersion})`);
|
|
2210
|
+
}
|
|
2211
|
+
if (result.rateLimited) {
|
|
2212
|
+
console.log(
|
|
2213
|
+
"(Checked recently with 60s rate limit; use --force to bypass)"
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
printThanks(options);
|
|
2217
|
+
return 0;
|
|
2218
|
+
}
|
|
2219
|
+
async function handleAutoUpdateCheck(currentVersion, options) {
|
|
2220
|
+
if (options.noUpdateCheck || options.checkUpdate || options.help || options.version || options.dryRun) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
const config = loadConfig();
|
|
2224
|
+
if (config.autoUpdate === false) {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
const update = await checkForUpdate(currentVersion);
|
|
2229
|
+
if (!update) return;
|
|
2230
|
+
if (options.json || options.yes || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2231
|
+
console.error(updateNotice(update));
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
console.log(
|
|
2235
|
+
`
|
|
2236
|
+
Update available \xB7 ${update.currentVersion} \u2192 ${update.latestVersion}
|
|
2237
|
+
Release notes: ${update.releaseUrl}`
|
|
2238
|
+
);
|
|
2239
|
+
const action = await select({
|
|
2240
|
+
message: "Update quitx?",
|
|
2241
|
+
options: [
|
|
2242
|
+
{ value: "update", label: "Update now", hint: "npm install --global" },
|
|
2243
|
+
{ value: "skip", label: "Skip" },
|
|
2244
|
+
{
|
|
2245
|
+
value: "ignore",
|
|
2246
|
+
label: "Skip this version",
|
|
2247
|
+
hint: `hide ${update.latestVersion}`
|
|
2248
|
+
}
|
|
2249
|
+
],
|
|
2250
|
+
initialValue: "update"
|
|
2251
|
+
});
|
|
2252
|
+
if (isCancel(action) || action === "skip") {
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (action === "ignore") {
|
|
2256
|
+
await ignoreUpdateVersion(update.latestVersion);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
console.log(`Updating to ${update.latestVersion}...`);
|
|
2260
|
+
try {
|
|
2261
|
+
await installUpdate(update.latestVersion);
|
|
2262
|
+
showOutro(`Updated to ${update.latestVersion}. Restart quitx to use it.`);
|
|
2263
|
+
process.exit(0);
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2266
|
+
console.error(`Failed to install update: ${msg}`);
|
|
2267
|
+
}
|
|
2268
|
+
} catch {
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
1301
2271
|
async function main(argv = process.argv.slice(2)) {
|
|
1302
2272
|
if (!isMacOS()) {
|
|
1303
|
-
console.error(
|
|
2273
|
+
console.error(
|
|
2274
|
+
"\u2716 quitx only works on macOS. Windows and Linux are not supported."
|
|
2275
|
+
);
|
|
1304
2276
|
return 1;
|
|
1305
2277
|
}
|
|
1306
2278
|
ensureMacOS();
|
|
@@ -1323,15 +2295,21 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1323
2295
|
printThanks(options);
|
|
1324
2296
|
return 0;
|
|
1325
2297
|
}
|
|
2298
|
+
const version = await getPackageVersion();
|
|
1326
2299
|
if (options.version) {
|
|
1327
|
-
const version = await getPackageVersion();
|
|
1328
2300
|
renderVersion(version);
|
|
1329
2301
|
printThanks(options);
|
|
1330
2302
|
return 0;
|
|
1331
2303
|
}
|
|
2304
|
+
if (options.checkUpdate) {
|
|
2305
|
+
return await handleManualUpdateCheck(version, options);
|
|
2306
|
+
}
|
|
2307
|
+
if (!options.noUpdateCheck && !options.json) {
|
|
2308
|
+
await handleAutoUpdateCheck(version, options);
|
|
2309
|
+
}
|
|
1332
2310
|
try {
|
|
1333
2311
|
if (options.manageConfig) {
|
|
1334
|
-
return await
|
|
2312
|
+
return await handleConfigCli(options);
|
|
1335
2313
|
}
|
|
1336
2314
|
if (options.manageExclude || options.exclude && options.exclude.length > 0) {
|
|
1337
2315
|
return await excludeCommand(options);
|
|
@@ -1352,7 +2330,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1352
2330
|
return 1;
|
|
1353
2331
|
}
|
|
1354
2332
|
}
|
|
1355
|
-
|
|
2333
|
+
var scriptName = process.argv[1] ? process.argv[1].replace(/\\/g, "/").split("/").pop()?.toLowerCase() : "";
|
|
2334
|
+
var isMain = scriptName === "cli.ts" || scriptName === "cli.js" || scriptName === "quitx" || scriptName === "quitx.cmd" || scriptName === "quitx.ps1" || scriptName === "quitx.exe";
|
|
2335
|
+
if (isMain) {
|
|
1356
2336
|
main().then((code) => {
|
|
1357
2337
|
if (code !== 0) {
|
|
1358
2338
|
process.exit(code);
|
|
@@ -1363,4 +2343,4 @@ if (process.argv[1] && (process.argv[1].endsWith("/cli.ts") || process.argv[1].e
|
|
|
1363
2343
|
});
|
|
1364
2344
|
}
|
|
1365
2345
|
|
|
1366
|
-
export { getPackageVersion, main, parseCliArgs };
|
|
2346
|
+
export { getPackageVersion, handleAutoUpdateCheck, handleManualUpdateCheck, main, parseCliArgs };
|