@coreify/quitx 1.0.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/dist/cli.js ADDED
@@ -0,0 +1,1366 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'fs/promises';
3
+ import { select, isCancel, spinner, log, intro, multiselect, outro, confirm, cancel } from '@clack/prompts';
4
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+ import { execa } from 'execa';
8
+ import { spawn } from 'child_process';
9
+ import { MultiSelectPrompt } from '@clack/core';
10
+
11
+ var CONFIG_DIR = join(homedir(), ".quitx");
12
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
13
+ var DEFAULT_CONFIG = {
14
+ exclude: [],
15
+ force: "normal",
16
+ includeFinder: false,
17
+ includeBackground: false
18
+ };
19
+ function loadConfig(filePath = CONFIG_FILE) {
20
+ try {
21
+ if (!existsSync(filePath)) {
22
+ return { ...DEFAULT_CONFIG, exclude: [] };
23
+ }
24
+ const raw = readFileSync(filePath, "utf-8");
25
+ const parsed = JSON.parse(raw);
26
+ if (typeof parsed !== "object" || parsed === null) {
27
+ return { ...DEFAULT_CONFIG, exclude: [] };
28
+ }
29
+ const obj = parsed;
30
+ return {
31
+ exclude: Array.isArray(obj.exclude) ? obj.exclude : [],
32
+ force: obj.force === "force" || obj.force === "normal" ? obj.force : DEFAULT_CONFIG.force,
33
+ includeFinder: typeof obj.includeFinder === "boolean" ? obj.includeFinder : DEFAULT_CONFIG.includeFinder,
34
+ includeBackground: typeof obj.includeBackground === "boolean" ? obj.includeBackground : DEFAULT_CONFIG.includeBackground
35
+ };
36
+ } catch {
37
+ return { ...DEFAULT_CONFIG, exclude: [] };
38
+ }
39
+ }
40
+ function saveConfig(config, filePath = CONFIG_FILE) {
41
+ try {
42
+ const dir = join(filePath, "..");
43
+ if (!existsSync(dir)) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
46
+ writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8");
47
+ } catch {
48
+ }
49
+ }
50
+ function addExcludedApps(apps, filePath = CONFIG_FILE) {
51
+ const current = loadConfig(filePath);
52
+ const set = new Set(current.exclude.map((a) => a.toLowerCase().trim()));
53
+ const updated = [...current.exclude];
54
+ for (const app of apps) {
55
+ const trimmed = app.trim();
56
+ if (trimmed && !set.has(trimmed.toLowerCase())) {
57
+ set.add(trimmed.toLowerCase());
58
+ updated.push(trimmed);
59
+ }
60
+ }
61
+ const next = { ...current, exclude: updated };
62
+ saveConfig(next, filePath);
63
+ return next;
64
+ }
65
+ function removeExcludedApps(apps, filePath = CONFIG_FILE) {
66
+ const current = loadConfig(filePath);
67
+ const toRemove = new Set(apps.map((a) => a.toLowerCase().trim()));
68
+ const updated = current.exclude.filter(
69
+ (app) => !toRemove.has(app.toLowerCase().trim())
70
+ );
71
+ const next = { ...current, exclude: updated };
72
+ saveConfig(next, filePath);
73
+ return next;
74
+ }
75
+
76
+ // src/macos/jxa.ts
77
+ var APPKIT_PREAMBLE = "ObjC.import('AppKit');";
78
+ var RUNNING_APPS = "$.NSWorkspace.sharedWorkspace.runningApplications";
79
+ function buildAppMatcherScript(body) {
80
+ const bodyIndented = body.trim().split("\n").map((line) => ` ${line}`).join("\n");
81
+ return `${APPKIT_PREAMBLE}
82
+ function run(argv) {
83
+ const apps = ${RUNNING_APPS};
84
+ for (let i = 0; i < apps.count; i++) {
85
+ const app = apps.objectAtIndex(i);
86
+ ${bodyIndented}
87
+ }
88
+ return 'false';
89
+ }`;
90
+ }
91
+ async function runJXA(script, args = [], executor = execa) {
92
+ const { stdout } = await executor("osascript", [
93
+ "-l",
94
+ "JavaScript",
95
+ "-e",
96
+ script,
97
+ ...args
98
+ ]);
99
+ return stdout.trim();
100
+ }
101
+
102
+ // src/macos/apps.ts
103
+ function buildDiscoveryScript(includeBackground = false) {
104
+ const bg = includeBackground ? "true" : "false";
105
+ return `${APPKIT_PREAMBLE}
106
+ function run() {
107
+ const includeBackground = ${bg};
108
+ const Regular = $.NSApplicationActivationPolicyRegular;
109
+ const apps = ${RUNNING_APPS};
110
+ const lines = [];
111
+ for (let i = 0; i < apps.count; i++) {
112
+ const app = apps.objectAtIndex(i);
113
+ if (!includeBackground && app.activationPolicy !== Regular) continue;
114
+ const name = app.localizedName ? app.localizedName.js : '';
115
+ const bundleId = app.bundleIdentifier ? app.bundleIdentifier.js : '';
116
+ const pid = String(app.processIdentifier);
117
+ lines.push(name + '\\t' + bundleId + '\\t' + pid);
118
+ }
119
+ return lines.join('\\n');
120
+ }`.trim();
121
+ }
122
+ var IS_APP_RUNNING_SCRIPT = buildAppMatcherScript(`
123
+ const name = argv[0] || '';
124
+ const bundleId = argv[1] || '';
125
+ if (bundleId && app.bundleIdentifier && app.bundleIdentifier.js === bundleId) {
126
+ return 'true';
127
+ }
128
+ if (name && app.localizedName && app.localizedName.js === name) {
129
+ return 'true';
130
+ }
131
+ `);
132
+ function formatDynamicName(identifier) {
133
+ const parts = identifier.split(".").filter(Boolean);
134
+ const last = parts[parts.length - 1];
135
+ if (!last || last.length <= 1) {
136
+ return identifier;
137
+ }
138
+ const spaced = last.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2");
139
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
140
+ }
141
+ function parseAppListOutput(stdout) {
142
+ const trimmed = stdout.trim();
143
+ if (!trimmed) {
144
+ return [];
145
+ }
146
+ if (trimmed.includes(" ") || trimmed.includes("\n")) {
147
+ const lines = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
148
+ const results = [];
149
+ for (const line of lines) {
150
+ const parts = line.split(" ");
151
+ const rawName = parts[0]?.trim();
152
+ if (!rawName) continue;
153
+ const rawBundle = parts[1]?.trim();
154
+ const bundleId = rawBundle && rawBundle !== "missing value" && rawBundle.length > 0 ? rawBundle : void 0;
155
+ const rawPid = parts[2]?.trim();
156
+ const pidNum = rawPid ? parseInt(rawPid, 10) : void 0;
157
+ const pid = !Number.isNaN(pidNum) && pidNum !== void 0 && pidNum > 0 ? pidNum : void 0;
158
+ let name = rawName;
159
+ if (bundleId && rawName === bundleId) {
160
+ name = formatDynamicName(bundleId);
161
+ }
162
+ results.push({ name, bundleId, pid });
163
+ }
164
+ return results;
165
+ }
166
+ return trimmed.split(",").map((name) => name.trim()).filter(Boolean).map((name) => ({ name }));
167
+ }
168
+ function sortApps(apps) {
169
+ return [...apps].sort(
170
+ (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
171
+ );
172
+ }
173
+ function filterApps(apps, optionsOrExclude = {}) {
174
+ const opts = Array.isArray(optionsOrExclude) ? { exclude: optionsOrExclude } : optionsOrExclude;
175
+ const exclude = opts.exclude ?? [];
176
+ const includeFinder = opts.includeFinder ?? false;
177
+ const seen = /* @__PURE__ */ new Set();
178
+ const excludeSet = new Set(exclude.map((e) => e.toLowerCase().trim()));
179
+ const filtered = [];
180
+ for (const app of apps) {
181
+ const nameLower = app.name.toLowerCase().trim();
182
+ const bundleLower = app.bundleId?.toLowerCase().trim();
183
+ if (nameLower === "quitx" || bundleLower === "com.kiron.quitx") {
184
+ continue;
185
+ }
186
+ if (!includeFinder) {
187
+ if (nameLower === "finder" || bundleLower === "com.apple.finder") {
188
+ continue;
189
+ }
190
+ }
191
+ if (excludeSet.has(nameLower) || bundleLower && excludeSet.has(bundleLower)) {
192
+ continue;
193
+ }
194
+ const key = bundleLower ?? nameLower;
195
+ if (seen.has(key)) {
196
+ continue;
197
+ }
198
+ seen.add(key);
199
+ filtered.push(app);
200
+ }
201
+ return sortApps(filtered);
202
+ }
203
+ async function getRunningApps(optionsOrExclude, executor) {
204
+ let exec;
205
+ let filterOpts = {};
206
+ let includeBackground = false;
207
+ if (typeof optionsOrExclude === "function") {
208
+ exec = optionsOrExclude;
209
+ } else if (Array.isArray(optionsOrExclude)) {
210
+ filterOpts = { exclude: optionsOrExclude };
211
+ exec = executor;
212
+ } else if (optionsOrExclude && typeof optionsOrExclude === "object") {
213
+ const opts = optionsOrExclude;
214
+ filterOpts = {
215
+ exclude: opts.exclude ?? [],
216
+ includeFinder: opts.includeFinder ?? false
217
+ };
218
+ includeBackground = opts.includeBackground ?? false;
219
+ exec = executor;
220
+ } else {
221
+ exec = executor;
222
+ }
223
+ const script = buildDiscoveryScript(includeBackground);
224
+ const stdout = await runJXA(script, [], exec);
225
+ const apps = parseAppListOutput(stdout);
226
+ return filterApps(apps, filterOpts);
227
+ }
228
+ function isProcessAlive(pid) {
229
+ try {
230
+ process.kill(pid, 0);
231
+ return true;
232
+ } catch (error) {
233
+ if (typeof error === "object" && error !== null && ("code" in error && error.code === "EPERM" || "message" in error && error.message.includes("EPERM"))) {
234
+ return true;
235
+ }
236
+ return false;
237
+ }
238
+ }
239
+ async function isAppRunning(target, executor) {
240
+ if (typeof target === "number") {
241
+ return isProcessAlive(target);
242
+ }
243
+ if (typeof target === "object" && target.pid !== void 0 && target.pid > 0) {
244
+ return isProcessAlive(target.pid);
245
+ }
246
+ const name = typeof target === "string" ? target : target.name;
247
+ const bundleId = typeof target === "object" ? target.bundleId ?? "" : "";
248
+ try {
249
+ const res = await runJXA(IS_APP_RUNNING_SCRIPT, [name, bundleId], executor);
250
+ return res.trim().toLowerCase() === "true";
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ // src/utils/sleep.ts
257
+ function sleep(ms) {
258
+ return new Promise((resolve) => setTimeout(resolve, ms));
259
+ }
260
+
261
+ // src/macos/terminal.ts
262
+ var KNOWN_TERMINALS = [
263
+ {
264
+ name: "Terminal",
265
+ aliases: ["apple_terminal", "terminal"],
266
+ bundleIds: ["com.apple.terminal"]
267
+ },
268
+ {
269
+ name: "iTerm2",
270
+ aliases: ["iterm.app", "iterm", "iterm2"],
271
+ bundleIds: ["com.googlecode.iterm2"]
272
+ },
273
+ {
274
+ name: "Warp",
275
+ aliases: ["warpterminal", "warp"],
276
+ bundleIds: ["dev.warp.warp-stable", "dev.warp.warp"]
277
+ },
278
+ {
279
+ name: "Ghostty",
280
+ aliases: ["ghostty"],
281
+ bundleIds: ["com.mitchellh.ghostty"]
282
+ },
283
+ {
284
+ name: "Alacritty",
285
+ aliases: ["alacritty"],
286
+ bundleIds: ["org.alacritty"]
287
+ },
288
+ {
289
+ name: "kitty",
290
+ aliases: ["kitty"],
291
+ bundleIds: ["net.kovidgoyal.kitty"]
292
+ },
293
+ {
294
+ name: "WezTerm",
295
+ aliases: ["wezterm", "wezterm-gui"],
296
+ bundleIds: ["com.github.wez.wezterm"]
297
+ },
298
+ {
299
+ name: "Hyper",
300
+ aliases: ["hyper"],
301
+ bundleIds: ["co.zeit.hyper"]
302
+ },
303
+ {
304
+ name: "Visual Studio Code",
305
+ aliases: ["vscode", "code"],
306
+ bundleIds: ["com.microsoft.vscode", "com.microsoft.vscodeinsiders"]
307
+ },
308
+ {
309
+ name: "Cursor",
310
+ aliases: ["cursor"],
311
+ bundleIds: ["com.todesktop.230313mzl4w4u92"]
312
+ }
313
+ ];
314
+ function getCurrentTerminalApp(env = process.env) {
315
+ const termProgram = env["TERM_PROGRAM"]?.toLowerCase().trim();
316
+ const terminalEmulator = env["TERMINAL_EMULATOR"]?.toLowerCase().trim();
317
+ const lcTerminal = env["LC_TERMINAL"]?.toLowerCase().trim();
318
+ const target = termProgram || terminalEmulator || lcTerminal;
319
+ if (!target) {
320
+ return null;
321
+ }
322
+ if (target.includes("warp")) {
323
+ return "Warp";
324
+ }
325
+ for (const term of KNOWN_TERMINALS) {
326
+ if (term.aliases.some(
327
+ (alias) => target === alias || alias !== "terminal" && target.includes(alias)
328
+ )) {
329
+ return term.name;
330
+ }
331
+ }
332
+ return env["TERM_PROGRAM"] || null;
333
+ }
334
+ function isCurrentTerminalApp(app, env = process.env) {
335
+ const current = getCurrentTerminalApp(env);
336
+ if (!current) {
337
+ return false;
338
+ }
339
+ const curLower = current.toLowerCase();
340
+ const appNameLower = app.name.toLowerCase();
341
+ const appBundleLower = app.bundleId?.toLowerCase();
342
+ if (appNameLower === curLower) {
343
+ return true;
344
+ }
345
+ for (const term of KNOWN_TERMINALS) {
346
+ if (term.name.toLowerCase() === curLower) {
347
+ if (appNameLower === term.name.toLowerCase() || appBundleLower && term.bundleIds.includes(appBundleLower)) {
348
+ return true;
349
+ }
350
+ }
351
+ }
352
+ return false;
353
+ }
354
+
355
+ // src/macos/quit.ts
356
+ var QUIT_JXA_SCRIPT = buildAppMatcherScript(`
357
+ const bundleId = argv[0] || '';
358
+ const name = argv[1] || '';
359
+ const bid = app.bundleIdentifier ? app.bundleIdentifier.js : '';
360
+ const nm = app.localizedName ? app.localizedName.js : '';
361
+ if ((bundleId && bid === bundleId) || (name && nm === name)) {
362
+ return String(app.terminate());
363
+ }
364
+ `);
365
+ function shellQuote(value) {
366
+ return `'${value.replace(/'/g, "'\\''")}'`;
367
+ }
368
+ function defaultDeferredQuitScheduler(app, force = false) {
369
+ try {
370
+ const bundleId = app.bundleId ?? "";
371
+ const cmd = force && app.pid ? `sleep 0.4 && kill -9 ${app.pid}` : `sleep 0.4 && osascript -l JavaScript -e ${shellQuote(QUIT_JXA_SCRIPT)} ${shellQuote(bundleId)} ${shellQuote(app.name)}`;
372
+ const child = spawn("sh", ["-c", cmd], {
373
+ detached: true,
374
+ stdio: "ignore"
375
+ });
376
+ child.unref();
377
+ } catch {
378
+ }
379
+ }
380
+ async function sendQuitSignal(app, executor) {
381
+ const res = await runJXA(
382
+ QUIT_JXA_SCRIPT,
383
+ [app.bundleId ?? "", app.name],
384
+ executor
385
+ );
386
+ if (res.trim().toLowerCase() !== "true") {
387
+ throw new Error(`Could not quit "${app.name}"`);
388
+ }
389
+ }
390
+ function forceQuitApp(app) {
391
+ if (app.pid && app.pid > 0) {
392
+ try {
393
+ process.kill(app.pid, "SIGKILL");
394
+ return true;
395
+ } catch {
396
+ return false;
397
+ }
398
+ }
399
+ return false;
400
+ }
401
+ async function quitApps(apps, options = {}, executor, deferredScheduler = defaultDeferredQuitScheduler) {
402
+ if (apps.length === 0) {
403
+ return [];
404
+ }
405
+ const currentTerminalApps = [];
406
+ const otherApps = [];
407
+ for (const app of apps) {
408
+ if (isCurrentTerminalApp(app)) {
409
+ currentTerminalApps.push(app);
410
+ } else {
411
+ otherApps.push(app);
412
+ }
413
+ }
414
+ const results = [];
415
+ if (otherApps.length > 0) {
416
+ const timeoutMs = options.timeoutMs ?? 700;
417
+ await Promise.allSettled(
418
+ otherApps.map((app) => sendQuitSignal(app, executor))
419
+ );
420
+ await sleep(timeoutMs);
421
+ for (const app of otherApps) {
422
+ const stillRunning = await isAppRunning(app, executor);
423
+ if (!stillRunning) {
424
+ results.push({ app, success: true, forced: false });
425
+ continue;
426
+ }
427
+ if (options.force) {
428
+ const killed = forceQuitApp(app);
429
+ if (killed) {
430
+ await sleep(100);
431
+ const aliveAfterForce = await isAppRunning(app, executor);
432
+ if (!aliveAfterForce) {
433
+ results.push({ app, success: true, forced: true });
434
+ continue;
435
+ }
436
+ }
437
+ results.push({
438
+ app,
439
+ success: false,
440
+ forced: true,
441
+ error: "App remained running after force quit"
442
+ });
443
+ continue;
444
+ }
445
+ results.push({
446
+ app,
447
+ success: false,
448
+ forced: false,
449
+ error: "App is still running (may have unsaved changes or prompt)"
450
+ });
451
+ }
452
+ }
453
+ for (const termApp of currentTerminalApps) {
454
+ deferredScheduler(termApp, options.force ?? false);
455
+ results.push({
456
+ app: termApp,
457
+ success: true,
458
+ forced: options.force ?? false
459
+ });
460
+ }
461
+ return results;
462
+ }
463
+ function showIntro() {
464
+ intro("quitx");
465
+ }
466
+ function showOutro(message) {
467
+ outro(message);
468
+ }
469
+ function showCancel(message = "Cancelled.") {
470
+ cancel(message);
471
+ }
472
+ function renderResults(results) {
473
+ for (const res of results) {
474
+ if (res.success) {
475
+ const label = res.forced ? `Force quit ${res.app.name}` : res.app.name;
476
+ log.success(label);
477
+ } else {
478
+ const reason = res.error ? ` (${res.error})` : "";
479
+ log.error(`Could not quit ${res.app.name}${reason}`);
480
+ }
481
+ }
482
+ }
483
+ function renderList(apps, json = false) {
484
+ if (json) {
485
+ console.log(JSON.stringify(apps, null, 2));
486
+ return;
487
+ }
488
+ for (const app of apps) {
489
+ console.log(app.name);
490
+ }
491
+ }
492
+ function renderHelp() {
493
+ console.log(
494
+ `
495
+ quitx - Quit running macOS apps from your terminal.
496
+
497
+ USAGE:
498
+ $ quitx [options] [apps]
499
+
500
+ OPTIONS:
501
+ -a, --all Quit all running GUI apps
502
+ -y, --yes Skip confirmation prompts
503
+ -l, --list List running GUI apps and exit
504
+ -f, --force Force quit apps (SIGKILL) if normal quit fails
505
+ -b, --background Include background processes in app list
506
+ --include-finder Include Finder in the app list
507
+ --exclude <apps> Add apps to persistent exclude list (comma-separated)
508
+ --config Open interactive config manager
509
+ --json Output data in JSON format
510
+ -h, --help Show help information
511
+ -v, --version Show version number
512
+
513
+ COMMANDS:
514
+ config Manage configuration interactively
515
+ exclude Manage excluded applications interactively
516
+
517
+ EXAMPLES:
518
+ $ quitx Interactive app selector
519
+ $ quitx --all Quit all running apps (with confirmation)
520
+ $ quitx --all --yes Quit all running apps immediately
521
+ $ quitx --list List currently running GUI apps
522
+ $ quitx --list --background List all apps including background processes
523
+ $ quitx --list --include-finder List apps including Finder
524
+ $ quitx config Open interactive config manager
525
+ $ quitx exclude Open interactive menu to manage excluded apps
526
+ $ quitx --exclude Spotify,Slack Add Spotify and Slack to exclude list
527
+ $ quitx Slack,Discord Quit specific apps directly
528
+ `.trim()
529
+ );
530
+ }
531
+ function renderVersion(version) {
532
+ console.log(`quitx v${version}`);
533
+ }
534
+ var THANKS_MESSAGE = "\nThanks for using quitx..!";
535
+ function printThanks(options) {
536
+ if (options?.json || options?.quiet) return;
537
+ console.log(THANKS_MESSAGE);
538
+ }
539
+ var SELECT_ALL_VALUE = "__QUIT_ALL_APPS__";
540
+ var BACK_VALUE = "back";
541
+ function backOption() {
542
+ return { value: BACK_VALUE, label: "Back to main menu" };
543
+ }
544
+ function handleQuitAllToggle(currentValue, selectedValues) {
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) {
612
+ if (apps.length === 0) {
613
+ return [];
614
+ }
615
+ const appOptions = apps.map((app) => {
616
+ const hint = app.bundleId ?? (app.pid ? `PID: ${app.pid}` : void 0);
617
+ return {
618
+ value: app.bundleId ?? `${app.name}-${app.pid ?? 0}`,
619
+ label: app.name,
620
+ ...hint ? { hint } : {}
621
+ };
622
+ });
623
+ const options = [
624
+ {
625
+ value: SELECT_ALL_VALUE,
626
+ label: "Quit all apps",
627
+ hint: `${apps.length} eligible apps`
628
+ },
629
+ ...appOptions
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
+ }
663
+ if (isCancel(selected) || !Array.isArray(selected)) {
664
+ return selected;
665
+ }
666
+ const selectedArray = selected;
667
+ if (selectedArray.length === 0) {
668
+ return [];
669
+ }
670
+ if (selectedArray.includes(SELECT_ALL_VALUE)) {
671
+ return [...apps];
672
+ }
673
+ const selectedSet = new Set(selectedArray);
674
+ return apps.filter((app) => {
675
+ const key = app.bundleId ?? `${app.name}-${app.pid ?? 0}`;
676
+ return selectedSet.has(key);
677
+ });
678
+ }
679
+ async function shouldConfirmQuit(count, isAll = false, forceYes = false) {
680
+ if (forceYes || count === 0) {
681
+ return true;
682
+ }
683
+ if (!isAll && count < 4) {
684
+ return true;
685
+ }
686
+ const message = isAll ? `Quit all ${count} apps?` : `Quit ${count} selected apps?`;
687
+ const confirmed = await confirm({
688
+ message,
689
+ initialValue: false
690
+ });
691
+ return confirmed;
692
+ }
693
+
694
+ // src/commands/all.ts
695
+ async function allCommand(options = {}) {
696
+ if (!options.json) {
697
+ showIntro();
698
+ }
699
+ const config = loadConfig();
700
+ const useForce = options.force ?? config.force === "force";
701
+ const includeFinder = options.includeFinder ?? config.includeFinder;
702
+ const includeBackground = options.includeBackground ?? config.includeBackground;
703
+ const apps = await getRunningApps({
704
+ exclude: config.exclude,
705
+ includeFinder,
706
+ includeBackground
707
+ });
708
+ if (apps.length === 0) {
709
+ if (options.json) {
710
+ console.log(JSON.stringify({ quit: 0, results: [] }, null, 2));
711
+ } else {
712
+ showOutro("No running apps found to quit.");
713
+ printThanks(options);
714
+ }
715
+ return 0;
716
+ }
717
+ if (!options.yes) {
718
+ const confirmation = await shouldConfirmQuit(apps.length, true, false);
719
+ if (isCancel(confirmation) || confirmation !== true) {
720
+ showCancel("Cancelled.");
721
+ printThanks(options);
722
+ return 0;
723
+ }
724
+ }
725
+ const s = spinner();
726
+ if (!options.json) {
727
+ s.start(`Quitting ${apps.length} apps...`);
728
+ }
729
+ const results = await quitApps(apps, { force: useForce });
730
+ if (!options.json) {
731
+ s.stop("Quitting complete");
732
+ renderResults(results);
733
+ const successCount = results.filter((r) => r.success).length;
734
+ showOutro(`Quit ${successCount} of ${apps.length} apps.`);
735
+ printThanks(options);
736
+ } else {
737
+ console.log(
738
+ JSON.stringify(
739
+ {
740
+ total: apps.length,
741
+ quit: results.filter((r) => r.success).length,
742
+ results
743
+ },
744
+ null,
745
+ 2
746
+ )
747
+ );
748
+ }
749
+ return 0;
750
+ }
751
+ async function toggleQuitMode() {
752
+ const config = loadConfig();
753
+ const choice = await select({
754
+ message: `Quit Mode (current: ${config.force})`,
755
+ options: [
756
+ {
757
+ value: "normal",
758
+ label: "Normal Quit",
759
+ hint: "Sends a graceful quit signal. Apps may prompt to save unsaved work before closing."
760
+ },
761
+ {
762
+ value: "force",
763
+ label: "Force Quit",
764
+ hint: "Immediately kills processes (SIGKILL). Unsaved work will be lost. Use when apps are unresponsive."
765
+ },
766
+ backOption()
767
+ ],
768
+ initialValue: config.force
769
+ });
770
+ if (isCancel(choice) || choice === BACK_VALUE) return;
771
+ config.force = choice;
772
+ saveConfig(config);
773
+ log.success(`Quit mode set to: ${config.force}`);
774
+ }
775
+ async function toggleIncludeFinder() {
776
+ const config = loadConfig();
777
+ const choice = await select({
778
+ message: `Include Finder (current: ${config.includeFinder ? "enabled" : "disabled"})`,
779
+ options: [
780
+ {
781
+ value: "disabled",
782
+ label: "Disabled",
783
+ hint: "Finder is excluded from the quit list. Recommended \u2014 Finder restarts automatically and quitting it rarely helps."
784
+ },
785
+ {
786
+ value: "enabled",
787
+ label: "Enabled",
788
+ hint: "Finder appears in the quit list like any other app. Useful if you want to fully restart Finder."
789
+ },
790
+ backOption()
791
+ ],
792
+ initialValue: config.includeFinder ? "enabled" : "disabled"
793
+ });
794
+ if (isCancel(choice) || choice === BACK_VALUE) return;
795
+ config.includeFinder = choice === "enabled";
796
+ saveConfig(config);
797
+ log.success(
798
+ `Include Finder: ${config.includeFinder ? "enabled" : "disabled"}`
799
+ );
800
+ }
801
+ async function toggleIncludeBackground() {
802
+ const config = loadConfig();
803
+ const choice = await select({
804
+ message: `Background Apps (current: ${config.includeBackground ? "enabled" : "disabled"})`,
805
+ options: [
806
+ {
807
+ value: "disabled",
808
+ label: "Disabled",
809
+ hint: "Only shows foreground GUI apps. This is the default and safest option \u2014 system daemons stay hidden."
810
+ },
811
+ {
812
+ value: "enabled",
813
+ label: "Enabled",
814
+ hint: "Shows ALL application processes including background agents and helpers. Use with caution \u2014 quitting system processes can cause instability."
815
+ },
816
+ backOption()
817
+ ],
818
+ initialValue: config.includeBackground ? "enabled" : "disabled"
819
+ });
820
+ if (isCancel(choice) || choice === BACK_VALUE) return;
821
+ config.includeBackground = choice === "enabled";
822
+ saveConfig(config);
823
+ log.success(
824
+ `Background apps: ${config.includeBackground ? "enabled" : "disabled"}`
825
+ );
826
+ }
827
+ async function manageExcludedApps() {
828
+ const choice = await select({
829
+ message: "Manage excluded apps",
830
+ options: [
831
+ { value: "add", label: "Add running apps to exclude list" },
832
+ { value: "remove", label: "Remove apps from exclude list" },
833
+ { value: "view", label: "View current exclude list" },
834
+ backOption()
835
+ ]
836
+ });
837
+ if (isCancel(choice) || choice === BACK_VALUE) return;
838
+ const config = loadConfig();
839
+ if (choice === "view") {
840
+ if (config.exclude.length === 0) {
841
+ log.info("No apps currently excluded.");
842
+ } else {
843
+ log.info(
844
+ `Excluded apps:
845
+ ${config.exclude.map((e) => ` - ${e}`).join("\n")}`
846
+ );
847
+ }
848
+ return;
849
+ }
850
+ if (choice === "add") {
851
+ const apps = await getRunningApps();
852
+ const currentSet = new Set(
853
+ config.exclude.map((e) => e.toLowerCase().trim())
854
+ );
855
+ const available = apps.filter(
856
+ (a) => !currentSet.has(a.name.toLowerCase().trim()) && !(a.bundleId && currentSet.has(a.bundleId.toLowerCase().trim()))
857
+ );
858
+ if (available.length === 0) {
859
+ log.info("All running apps are already excluded.");
860
+ return;
861
+ }
862
+ const selected = await multiselect({
863
+ message: "Select apps to exclude from quitting",
864
+ options: available.map((a) => ({
865
+ value: a.name,
866
+ label: a.name,
867
+ ...a.bundleId ? { hint: a.bundleId } : {}
868
+ })),
869
+ required: false
870
+ });
871
+ if (isCancel(selected)) return;
872
+ if (!Array.isArray(selected) || selected.length === 0) {
873
+ log.info("No apps selected.");
874
+ return;
875
+ }
876
+ addExcludedApps(selected);
877
+ log.success(`Added ${selected.length} apps to exclude list.`);
878
+ return;
879
+ }
880
+ if (choice === "remove") {
881
+ if (config.exclude.length === 0) {
882
+ log.info("No apps currently in exclude list.");
883
+ return;
884
+ }
885
+ const selected = await multiselect({
886
+ message: "Select apps to remove from exclude list",
887
+ options: config.exclude.map((name) => ({
888
+ value: name,
889
+ label: name
890
+ })),
891
+ required: false
892
+ });
893
+ if (isCancel(selected)) return;
894
+ if (!Array.isArray(selected) || selected.length === 0) {
895
+ log.info("No apps selected.");
896
+ return;
897
+ }
898
+ removeExcludedApps(selected);
899
+ log.success(`Removed ${selected.length} apps from exclude list.`);
900
+ }
901
+ }
902
+ function resetConfig() {
903
+ saveConfig({ ...DEFAULT_CONFIG, exclude: [] });
904
+ log.success("Config reset to defaults.");
905
+ }
906
+ async function configCommand() {
907
+ showIntro();
908
+ let running = true;
909
+ while (running) {
910
+ const config = loadConfig();
911
+ const choice = await select({
912
+ message: "Configuration",
913
+ options: [
914
+ {
915
+ value: "quit-mode",
916
+ label: `Toggle Quit Mode`,
917
+ hint: `current: ${config.force}`
918
+ },
919
+ {
920
+ value: "finder",
921
+ label: `Toggle Include Finder`,
922
+ hint: `current: ${config.includeFinder ? "enabled" : "disabled"}`
923
+ },
924
+ {
925
+ value: "background",
926
+ label: `Toggle Background Apps`,
927
+ hint: `current: ${config.includeBackground ? "enabled" : "disabled"}`
928
+ },
929
+ {
930
+ value: "exclude",
931
+ label: "Manage Excluded Apps",
932
+ hint: `${config.exclude.length} excluded`
933
+ },
934
+ {
935
+ value: "reset",
936
+ label: "Reset to Defaults",
937
+ hint: "restore all settings"
938
+ },
939
+ { value: "exit", label: "Exit" }
940
+ ]
941
+ });
942
+ if (isCancel(choice) || choice === "exit") {
943
+ running = false;
944
+ continue;
945
+ }
946
+ if (choice === "quit-mode") await toggleQuitMode();
947
+ if (choice === "finder") await toggleIncludeFinder();
948
+ if (choice === "background") await toggleIncludeBackground();
949
+ if (choice === "exclude") await manageExcludedApps();
950
+ if (choice === "reset") resetConfig();
951
+ }
952
+ showOutro("Config saved.");
953
+ printThanks();
954
+ return 0;
955
+ }
956
+ async function manageExcludeInteractive() {
957
+ showIntro();
958
+ const choice = await select({
959
+ message: "Manage excluded apps",
960
+ options: [
961
+ { value: "add", label: "Add running apps to exclude list" },
962
+ { value: "remove", label: "Remove apps from exclude list" },
963
+ { value: "view", label: "View current exclude list" }
964
+ ]
965
+ });
966
+ if (isCancel(choice)) {
967
+ showCancel("Cancelled.");
968
+ printThanks();
969
+ return 0;
970
+ }
971
+ const currentConfig = loadConfig();
972
+ if (choice === "view") {
973
+ if (currentConfig.exclude.length === 0) {
974
+ showOutro("No apps currently excluded in config.json.");
975
+ } else {
976
+ showOutro(
977
+ `Excluded apps:
978
+ ${currentConfig.exclude.map((e) => ` - ${e}`).join("\n")}`
979
+ );
980
+ }
981
+ printThanks();
982
+ return 0;
983
+ }
984
+ if (choice === "add") {
985
+ const apps = await getRunningApps();
986
+ const currentSet = new Set(
987
+ currentConfig.exclude.map((e) => e.toLowerCase().trim())
988
+ );
989
+ const available = apps.filter(
990
+ (a) => !currentSet.has(a.name.toLowerCase().trim()) && !(a.bundleId && currentSet.has(a.bundleId.toLowerCase().trim()))
991
+ );
992
+ if (available.length === 0) {
993
+ showOutro("All running apps are already excluded.");
994
+ printThanks();
995
+ return 0;
996
+ }
997
+ const selected = await multiselect({
998
+ message: "Select apps to exclude from quitting",
999
+ options: available.map((a) => ({
1000
+ value: a.name,
1001
+ label: a.name,
1002
+ ...a.bundleId ? { hint: a.bundleId } : {}
1003
+ })),
1004
+ required: false
1005
+ });
1006
+ if (isCancel(selected)) {
1007
+ showCancel("Cancelled.");
1008
+ printThanks();
1009
+ return 0;
1010
+ }
1011
+ if (!Array.isArray(selected) || selected.length === 0) {
1012
+ showOutro("No apps selected.");
1013
+ printThanks();
1014
+ return 0;
1015
+ }
1016
+ addExcludedApps(selected);
1017
+ showOutro(`Added ${selected.length} apps to exclude list.`);
1018
+ printThanks();
1019
+ return 0;
1020
+ }
1021
+ if (choice === "remove") {
1022
+ if (currentConfig.exclude.length === 0) {
1023
+ showOutro("No apps currently in exclude list.");
1024
+ printThanks();
1025
+ return 0;
1026
+ }
1027
+ const selected = await multiselect({
1028
+ message: "Select apps to remove from exclude list",
1029
+ options: currentConfig.exclude.map((name) => ({
1030
+ value: name,
1031
+ label: name
1032
+ })),
1033
+ required: false
1034
+ });
1035
+ if (isCancel(selected)) {
1036
+ showCancel("Cancelled.");
1037
+ printThanks();
1038
+ return 0;
1039
+ }
1040
+ if (!Array.isArray(selected) || selected.length === 0) {
1041
+ showOutro("No apps selected.");
1042
+ printThanks();
1043
+ return 0;
1044
+ }
1045
+ removeExcludedApps(selected);
1046
+ showOutro(`Removed ${selected.length} apps from exclude list.`);
1047
+ printThanks();
1048
+ return 0;
1049
+ }
1050
+ return 0;
1051
+ }
1052
+ async function excludeCommand(options = {}) {
1053
+ if (options.exclude && options.exclude.length > 0) {
1054
+ const updated = addExcludedApps(options.exclude);
1055
+ if (options.json) {
1056
+ console.log(JSON.stringify(updated, null, 2));
1057
+ } else {
1058
+ showIntro();
1059
+ showOutro(
1060
+ `Added to exclude list:
1061
+ ${options.exclude.map((e) => ` - ${e}`).join("\n")}`
1062
+ );
1063
+ printThanks(options);
1064
+ }
1065
+ return 0;
1066
+ }
1067
+ return await manageExcludeInteractive();
1068
+ }
1069
+ async function interactiveCommand(options = {}) {
1070
+ if (!options.json) {
1071
+ showIntro();
1072
+ }
1073
+ const s = spinner();
1074
+ if (!options.json) {
1075
+ s.start("Scanning running applications...");
1076
+ }
1077
+ const config = loadConfig();
1078
+ const useForce = options.force ?? config.force === "force";
1079
+ const includeFinder = options.includeFinder ?? config.includeFinder;
1080
+ const includeBackground = options.includeBackground ?? config.includeBackground;
1081
+ const apps = await getRunningApps({
1082
+ exclude: config.exclude,
1083
+ includeFinder,
1084
+ includeBackground
1085
+ });
1086
+ if (!options.json) {
1087
+ s.stop(
1088
+ `Found ${apps.length} running ${apps.length === 1 ? "app" : "apps"}`
1089
+ );
1090
+ }
1091
+ if (apps.length === 0) {
1092
+ if (options.json) {
1093
+ console.log(JSON.stringify({ quit: 0, results: [] }, null, 2));
1094
+ } else {
1095
+ showOutro("No running GUI applications found to quit.");
1096
+ printThanks(options);
1097
+ }
1098
+ return 0;
1099
+ }
1100
+ let targetApps;
1101
+ if (options.apps && options.apps.length > 0) {
1102
+ const targets = new Set(options.apps.map((a) => a.toLowerCase().trim()));
1103
+ targetApps = apps.filter(
1104
+ (app) => targets.has(app.name.toLowerCase().trim()) || app.bundleId && targets.has(app.bundleId.toLowerCase().trim())
1105
+ );
1106
+ if (targetApps.length === 0) {
1107
+ if (options.json) {
1108
+ console.log(
1109
+ JSON.stringify(
1110
+ { quit: 0, results: [], error: "No matching apps running" },
1111
+ null,
1112
+ 2
1113
+ )
1114
+ );
1115
+ } else {
1116
+ log.warn("None of the specified applications are currently running.");
1117
+ showOutro("Nothing to quit.");
1118
+ printThanks(options);
1119
+ }
1120
+ return 0;
1121
+ }
1122
+ } else {
1123
+ const selected = await selectApps(apps);
1124
+ if (typeof selected === "symbol" || isCancel(selected)) {
1125
+ showCancel("Cancelled.");
1126
+ printThanks(options);
1127
+ return 0;
1128
+ }
1129
+ if (selected.length === 0) {
1130
+ showOutro("No apps selected.");
1131
+ printThanks(options);
1132
+ return 0;
1133
+ }
1134
+ targetApps = selected;
1135
+ }
1136
+ if (!options.yes) {
1137
+ const confirmation = await shouldConfirmQuit(
1138
+ targetApps.length,
1139
+ targetApps.length === apps.length,
1140
+ false
1141
+ );
1142
+ if (isCancel(confirmation) || confirmation !== true) {
1143
+ showCancel("Cancelled.");
1144
+ printThanks(options);
1145
+ return 0;
1146
+ }
1147
+ }
1148
+ if (!options.json) {
1149
+ s.start(
1150
+ `Quitting ${targetApps.length} ${targetApps.length === 1 ? "app" : "apps"}...`
1151
+ );
1152
+ }
1153
+ const results = await quitApps(targetApps, { force: useForce });
1154
+ if (!options.json) {
1155
+ s.stop("Quitting complete");
1156
+ renderResults(results);
1157
+ const successCount = results.filter((r) => r.success).length;
1158
+ showOutro(`Done. Quit ${successCount} of ${targetApps.length} apps.`);
1159
+ printThanks(options);
1160
+ } else {
1161
+ console.log(
1162
+ JSON.stringify(
1163
+ {
1164
+ total: targetApps.length,
1165
+ quit: results.filter((r) => r.success).length,
1166
+ results
1167
+ },
1168
+ null,
1169
+ 2
1170
+ )
1171
+ );
1172
+ }
1173
+ return 0;
1174
+ }
1175
+
1176
+ // src/commands/list.ts
1177
+ async function listCommand(options = {}) {
1178
+ const config = loadConfig();
1179
+ const includeFinder = options.includeFinder ?? config.includeFinder;
1180
+ const includeBackground = options.includeBackground ?? config.includeBackground;
1181
+ const apps = await getRunningApps({
1182
+ exclude: config.exclude,
1183
+ includeFinder,
1184
+ includeBackground
1185
+ });
1186
+ renderList(apps, options.json);
1187
+ return 0;
1188
+ }
1189
+
1190
+ // src/utils/platform.ts
1191
+ function isMacOS(platform = process.platform) {
1192
+ return platform === "darwin";
1193
+ }
1194
+ function ensureMacOS(platform = process.platform) {
1195
+ if (!isMacOS(platform)) {
1196
+ throw new Error("quitx only works on macOS.");
1197
+ }
1198
+ }
1199
+
1200
+ // src/cli.ts
1201
+ async function getPackageVersion() {
1202
+ for (const url of [
1203
+ new URL("../package.json", import.meta.url),
1204
+ new URL("../../package.json", import.meta.url)
1205
+ ]) {
1206
+ try {
1207
+ const content = await readFile(url, "utf-8");
1208
+ const parsed = JSON.parse(content);
1209
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") {
1210
+ return parsed.version;
1211
+ }
1212
+ } catch {
1213
+ }
1214
+ }
1215
+ return "unknown";
1216
+ }
1217
+ function parseCliArgs(args) {
1218
+ const options = {
1219
+ apps: [],
1220
+ exclude: []
1221
+ };
1222
+ const rawPositional = [];
1223
+ for (let i = 0; i < args.length; i++) {
1224
+ const arg = args[i];
1225
+ if (!arg) continue;
1226
+ if (arg === "config") {
1227
+ options.manageConfig = true;
1228
+ } else if (arg === "exclude") {
1229
+ options.manageExclude = true;
1230
+ } else if (arg === "-a" || arg === "--all") {
1231
+ options.all = true;
1232
+ } else if (arg === "-y" || arg === "--yes") {
1233
+ options.yes = true;
1234
+ } else if (arg === "-l" || arg === "--list") {
1235
+ options.list = true;
1236
+ } else if (arg === "-f" || arg === "--force") {
1237
+ options.force = true;
1238
+ } else if (arg === "-h" || arg === "--help") {
1239
+ options.help = true;
1240
+ } else if (arg === "-v" || arg === "--version") {
1241
+ options.version = true;
1242
+ } else if (arg === "--json") {
1243
+ options.json = true;
1244
+ } else if (arg === "--include-finder") {
1245
+ options.includeFinder = true;
1246
+ } else if (arg === "-b" || arg === "--background") {
1247
+ options.includeBackground = true;
1248
+ } else if (arg === "--config") {
1249
+ options.manageConfig = true;
1250
+ } else if (arg === "--exclude") {
1251
+ const next = args[++i];
1252
+ if (next) {
1253
+ const split = next.split(",").map((s) => s.trim()).filter(Boolean);
1254
+ options.exclude?.push(...split);
1255
+ }
1256
+ } else if (arg.startsWith("--exclude=")) {
1257
+ const val = arg.slice("--exclude=".length);
1258
+ if (val) {
1259
+ const split = val.split(",").map((s) => s.trim()).filter(Boolean);
1260
+ options.exclude?.push(...split);
1261
+ }
1262
+ } else if (arg.startsWith("-")) {
1263
+ continue;
1264
+ } else {
1265
+ rawPositional.push(arg);
1266
+ }
1267
+ }
1268
+ if (rawPositional.length > 0) {
1269
+ if (options.manageExclude) {
1270
+ if (rawPositional.length > 1 && !rawPositional.some((a) => a.includes(","))) {
1271
+ throw new Error(
1272
+ "Multiple exclude applications must be comma-separated, not space-separated. Example: quitx exclude Spotify,Discord"
1273
+ );
1274
+ }
1275
+ const combined = rawPositional.join(" ");
1276
+ const split = combined.split(",").map((s) => s.trim()).filter(Boolean);
1277
+ if (split.length === 0) {
1278
+ throw new Error(
1279
+ "No application name specified to exclude. Example: quitx exclude Spotify,Discord"
1280
+ );
1281
+ }
1282
+ options.exclude?.push(...split);
1283
+ } else {
1284
+ if (rawPositional.length > 1 && !rawPositional.some((a) => a.includes(","))) {
1285
+ throw new Error(
1286
+ 'Multiple applications must be comma-separated, not space-separated. Example: quitx Slack,Discord. For names with spaces, use quotes: quitx "Google Chrome"'
1287
+ );
1288
+ }
1289
+ const combined = rawPositional.join(" ");
1290
+ const split = combined.split(",").map((s) => s.trim()).filter(Boolean);
1291
+ if (split.length === 0) {
1292
+ throw new Error(
1293
+ 'No application name specified. Example: quitx Slack,Discord. For names with spaces, use quotes: quitx "Google Chrome"'
1294
+ );
1295
+ }
1296
+ options.apps?.push(...split);
1297
+ }
1298
+ }
1299
+ return options;
1300
+ }
1301
+ async function main(argv = process.argv.slice(2)) {
1302
+ if (!isMacOS()) {
1303
+ console.error("\u2716 quitx only works on macOS.");
1304
+ return 1;
1305
+ }
1306
+ ensureMacOS();
1307
+ let options = {};
1308
+ try {
1309
+ options = parseCliArgs(argv);
1310
+ } catch (error) {
1311
+ const msg = error instanceof Error ? error.message : String(error);
1312
+ console.error(`\u2716 Error: ${msg}`);
1313
+ return 1;
1314
+ }
1315
+ if (process.stdin.isTTY) {
1316
+ process.once("SIGINT", () => {
1317
+ printThanks(options);
1318
+ process.exit(130);
1319
+ });
1320
+ }
1321
+ if (options.help) {
1322
+ renderHelp();
1323
+ printThanks(options);
1324
+ return 0;
1325
+ }
1326
+ if (options.version) {
1327
+ const version = await getPackageVersion();
1328
+ renderVersion(version);
1329
+ printThanks(options);
1330
+ return 0;
1331
+ }
1332
+ try {
1333
+ if (options.manageConfig) {
1334
+ return await configCommand();
1335
+ }
1336
+ if (options.manageExclude || options.exclude && options.exclude.length > 0) {
1337
+ return await excludeCommand(options);
1338
+ }
1339
+ if (options.list) {
1340
+ const code = await listCommand(options);
1341
+ printThanks(options);
1342
+ return code;
1343
+ }
1344
+ if (options.all) {
1345
+ return await allCommand(options);
1346
+ }
1347
+ return await interactiveCommand(options);
1348
+ } catch (error) {
1349
+ const msg = error instanceof Error ? error.message : String(error);
1350
+ console.error(`\u2716 Error: ${msg}`);
1351
+ printThanks(options);
1352
+ return 1;
1353
+ }
1354
+ }
1355
+ if (process.argv[1] && (process.argv[1].endsWith("/cli.ts") || process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("/quitx"))) {
1356
+ main().then((code) => {
1357
+ if (code !== 0) {
1358
+ process.exit(code);
1359
+ }
1360
+ }).catch((err) => {
1361
+ console.error(err);
1362
+ process.exit(1);
1363
+ });
1364
+ }
1365
+
1366
+ export { getPackageVersion, main, parseCliArgs };