pyre-cli 10.2.0 → 10.4.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/README.md +75 -18
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +737 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +183 -9
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfigDir,
|
|
3
|
+
readConfig,
|
|
4
|
+
writeConfig
|
|
5
|
+
} from "./chunk-LDJB5V4N.js";
|
|
6
|
+
import "./chunk-3RG5ZIWI.js";
|
|
7
|
+
|
|
8
|
+
// src/optimizeCmd.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import readline from "readline/promises";
|
|
13
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
14
|
+
import { promisify } from "util";
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
var execFileAsync = promisify(execFile);
|
|
17
|
+
var BUILTIN_PROTECTED = [
|
|
18
|
+
"com.apple.finder",
|
|
19
|
+
"com.apple.Terminal",
|
|
20
|
+
"com.googlecode.iterm2",
|
|
21
|
+
"dev.warp.Warp-Stable",
|
|
22
|
+
"com.mitchellh.ghostty",
|
|
23
|
+
"net.kovidgoyal.kitty",
|
|
24
|
+
"com.github.wez.wezterm",
|
|
25
|
+
"io.alacritty",
|
|
26
|
+
"com.apple.systempreferences"
|
|
27
|
+
];
|
|
28
|
+
var MEDIA_BUNDLES = /* @__PURE__ */ new Set([
|
|
29
|
+
"com.apple.music",
|
|
30
|
+
"com.spotify.client",
|
|
31
|
+
"com.apple.podcasts",
|
|
32
|
+
"com.apple.tv",
|
|
33
|
+
"com.colliderli.iina",
|
|
34
|
+
"org.videolan.vlc"
|
|
35
|
+
]);
|
|
36
|
+
var DEFAULT_CPU_BUSY_PCT = 5;
|
|
37
|
+
var MEDIA_CPU_BUSY_PCT = 1;
|
|
38
|
+
var TRACK_INTERVAL_MS = 5e3;
|
|
39
|
+
var TRACK_FRESH_MS = 12e4;
|
|
40
|
+
var TRACKER_LABEL = "com.pyre.optimize-tracker";
|
|
41
|
+
var TRACKER_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${TRACKER_LABEL}.plist`);
|
|
42
|
+
function usageFile() {
|
|
43
|
+
return path.join(getConfigDir(), "app-usage.json");
|
|
44
|
+
}
|
|
45
|
+
function parseDuration(input, fallbackMs) {
|
|
46
|
+
if (input === void 0 || input === null || String(input).trim() === "") return fallbackMs;
|
|
47
|
+
const m = String(input).trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)?$/);
|
|
48
|
+
if (!m) return NaN;
|
|
49
|
+
const n = parseFloat(m[1]);
|
|
50
|
+
const unit = m[2] ?? "m";
|
|
51
|
+
if (unit.startsWith("s")) return n * 1e3;
|
|
52
|
+
if (unit.startsWith("h")) return n * 36e5;
|
|
53
|
+
if (unit.startsWith("d")) return n * 864e5;
|
|
54
|
+
return n * 6e4;
|
|
55
|
+
}
|
|
56
|
+
function formatDuration(ms) {
|
|
57
|
+
const totalMin = Math.floor(ms / 6e4);
|
|
58
|
+
if (totalMin < 1) return `${Math.max(0, Math.floor(ms / 1e3))}s`;
|
|
59
|
+
const d = Math.floor(totalMin / 1440);
|
|
60
|
+
const h = Math.floor(totalMin % 1440 / 60);
|
|
61
|
+
const m = totalMin % 60;
|
|
62
|
+
if (d > 0) return `${d}d ${h}h`;
|
|
63
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
64
|
+
return `${m}m`;
|
|
65
|
+
}
|
|
66
|
+
function formatKb(kb) {
|
|
67
|
+
const bytes = kb * 1024;
|
|
68
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
69
|
+
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(0)} MB`;
|
|
70
|
+
return `${Math.round(kb)} KB`;
|
|
71
|
+
}
|
|
72
|
+
function parseAsnList(raw) {
|
|
73
|
+
const out = [];
|
|
74
|
+
const re = /ASN:(0x[0-9a-f]+-0x[0-9a-f]+)/gi;
|
|
75
|
+
let m;
|
|
76
|
+
while ((m = re.exec(raw)) !== null) {
|
|
77
|
+
const asn = `ASN:${m[1].toLowerCase()}`;
|
|
78
|
+
if (!out.includes(asn)) out.push(asn);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function parseLsappinfoList(raw) {
|
|
83
|
+
const apps = [];
|
|
84
|
+
const blocks = raw.split(/\r?\n(?=\s*\d+\)\s+")/);
|
|
85
|
+
for (const block of blocks) {
|
|
86
|
+
const header2 = block.match(/^\s*\d+\)\s+"(.*?)"\s+ASN:(0x[0-9a-f]+-0x[0-9a-f]+):/i);
|
|
87
|
+
if (!header2) continue;
|
|
88
|
+
if (!/type="Foreground"/.test(block)) continue;
|
|
89
|
+
const pidMatch = block.match(/\bpid\s*=\s*(\d+)/);
|
|
90
|
+
if (!pidMatch) continue;
|
|
91
|
+
const bundleMatch = block.match(/bundleID="([^"]+)"/);
|
|
92
|
+
const checkin = block.match(/checkin time = (\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})/);
|
|
93
|
+
let launchedAt;
|
|
94
|
+
if (checkin) {
|
|
95
|
+
const [, y, mo, d, h, mi, s] = checkin;
|
|
96
|
+
launchedAt = new Date(+y, +mo - 1, +d, +h, +mi, +s).getTime();
|
|
97
|
+
}
|
|
98
|
+
apps.push({
|
|
99
|
+
asn: `ASN:${header2[2].toLowerCase()}`,
|
|
100
|
+
name: header2[1],
|
|
101
|
+
bundleId: bundleMatch?.[1] ?? "",
|
|
102
|
+
pid: parseInt(pidMatch[1], 10),
|
|
103
|
+
launchedAt
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return apps;
|
|
107
|
+
}
|
|
108
|
+
function isTrackingFresh(usage, now) {
|
|
109
|
+
return !!usage && now - usage.updatedAt < TRACK_FRESH_MS;
|
|
110
|
+
}
|
|
111
|
+
function computeIdleMs(app, usage, now) {
|
|
112
|
+
if (app.frontmost) return 0;
|
|
113
|
+
if (!isTrackingFresh(usage, now)) return void 0;
|
|
114
|
+
const last = usage.apps[app.bundleId]?.lastActive ?? 0;
|
|
115
|
+
const base = Math.max(usage.trackingSince, app.launchedAt ?? 0);
|
|
116
|
+
return Math.max(0, now - Math.max(last, base));
|
|
117
|
+
}
|
|
118
|
+
function isProtected(app, protectedApps) {
|
|
119
|
+
const list = [...BUILTIN_PROTECTED, ...protectedApps].map((s) => s.toLowerCase());
|
|
120
|
+
return list.includes(app.bundleId.toLowerCase()) || list.includes(app.name.toLowerCase());
|
|
121
|
+
}
|
|
122
|
+
function selectIdleApps(apps, ctx) {
|
|
123
|
+
const candidates = [];
|
|
124
|
+
const skipped = [];
|
|
125
|
+
const fresh = isTrackingFresh(ctx.usage, ctx.now);
|
|
126
|
+
const busyPct = ctx.cpuBusyPct ?? DEFAULT_CPU_BUSY_PCT;
|
|
127
|
+
for (const app of [...apps].sort((a, b) => a.rank - b.rank)) {
|
|
128
|
+
if (app.frontmost) {
|
|
129
|
+
skipped.push({ app, reason: "frontmost" });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (isProtected(app, ctx.protectedApps)) {
|
|
133
|
+
skipped.push({ app, reason: "protected" });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ctx.selfPids.has(app.pid)) {
|
|
137
|
+
skipped.push({ app, reason: "running pyre" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (app.rank < ctx.keepRecent) {
|
|
141
|
+
skipped.push({ app, reason: `top ${ctx.keepRecent} recent` });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const threshold = MEDIA_BUNDLES.has(app.bundleId.toLowerCase()) ? MEDIA_CPU_BUSY_PCT : busyPct;
|
|
145
|
+
if (app.cpu >= threshold) {
|
|
146
|
+
skipped.push({ app, reason: `busy (${app.cpu.toFixed(1)}% CPU)` });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (app.launchedAt && ctx.now - app.launchedAt < ctx.idleThresholdMs) {
|
|
150
|
+
skipped.push({ app, reason: `launched ${formatDuration(ctx.now - app.launchedAt)} ago` });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (fresh) {
|
|
154
|
+
const idleMs = computeIdleMs(app, ctx.usage, ctx.now);
|
|
155
|
+
if (idleMs < ctx.idleThresholdMs) {
|
|
156
|
+
skipped.push({ app, reason: `used ${formatDuration(idleMs)} ago` });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
candidates.push({ app, idleMs });
|
|
160
|
+
} else {
|
|
161
|
+
candidates.push({ app });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { candidates, skipped };
|
|
165
|
+
}
|
|
166
|
+
async function run(cmd, args, timeoutMs = 8e3) {
|
|
167
|
+
try {
|
|
168
|
+
const { stdout } = await execFileAsync(cmd, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
|
|
169
|
+
return stdout;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return typeof err?.stdout === "string" ? err.stdout : "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function listRunningApps() {
|
|
175
|
+
const [listRaw, visibleRaw, frontRaw] = await Promise.all([
|
|
176
|
+
run("lsappinfo", ["list"]),
|
|
177
|
+
run("lsappinfo", ["visibleProcessList"]),
|
|
178
|
+
run("lsappinfo", ["front"])
|
|
179
|
+
]);
|
|
180
|
+
const base = parseLsappinfoList(listRaw);
|
|
181
|
+
const order = parseAsnList(visibleRaw);
|
|
182
|
+
const front = parseAsnList(frontRaw)[0];
|
|
183
|
+
const stats = /* @__PURE__ */ new Map();
|
|
184
|
+
if (base.length) {
|
|
185
|
+
const ps = await run("ps", ["-axo", "pid=,ppid=,%cpu=,rss="]);
|
|
186
|
+
const children = /* @__PURE__ */ new Map();
|
|
187
|
+
const own = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const line of ps.split("\n")) {
|
|
189
|
+
const [pid, ppid, cpu, rss] = line.trim().split(/\s+/);
|
|
190
|
+
if (!pid) continue;
|
|
191
|
+
const p = parseInt(pid, 10);
|
|
192
|
+
const pp = parseInt(ppid, 10);
|
|
193
|
+
own.set(p, { cpu: parseFloat(cpu) || 0, rss: parseInt(rss, 10) || 0 });
|
|
194
|
+
if (!children.has(pp)) children.set(pp, []);
|
|
195
|
+
children.get(pp).push(p);
|
|
196
|
+
}
|
|
197
|
+
for (const app of base) {
|
|
198
|
+
if (!own.has(app.pid)) continue;
|
|
199
|
+
let cpu = 0;
|
|
200
|
+
let rss = 0;
|
|
201
|
+
const stack = [app.pid];
|
|
202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
203
|
+
while (stack.length) {
|
|
204
|
+
const p = stack.pop();
|
|
205
|
+
if (seen.has(p)) continue;
|
|
206
|
+
seen.add(p);
|
|
207
|
+
const s = own.get(p);
|
|
208
|
+
if (s) {
|
|
209
|
+
cpu += s.cpu;
|
|
210
|
+
rss += s.rss;
|
|
211
|
+
}
|
|
212
|
+
stack.push(...children.get(p) ?? []);
|
|
213
|
+
}
|
|
214
|
+
stats.set(app.pid, { cpu, rss });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const unranked = base.filter((a) => !order.includes(a.asn)).sort((a, b) => (b.launchedAt ?? 0) - (a.launchedAt ?? 0));
|
|
218
|
+
return base.filter((a) => stats.has(a.pid)).map((a) => {
|
|
219
|
+
const idx = order.indexOf(a.asn);
|
|
220
|
+
const s = stats.get(a.pid);
|
|
221
|
+
return {
|
|
222
|
+
...a,
|
|
223
|
+
rank: idx >= 0 ? idx : order.length + unranked.indexOf(a),
|
|
224
|
+
cpu: s.cpu,
|
|
225
|
+
rssKb: s.rss,
|
|
226
|
+
frontmost: a.asn === front
|
|
227
|
+
};
|
|
228
|
+
}).sort((a, b) => a.rank - b.rank);
|
|
229
|
+
}
|
|
230
|
+
async function getSelfPids() {
|
|
231
|
+
const raw = await run("ps", ["-axo", "pid=,ppid="]);
|
|
232
|
+
const parent = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const line of raw.split("\n")) {
|
|
234
|
+
const [pid, ppid] = line.trim().split(/\s+/).map((n) => parseInt(n, 10));
|
|
235
|
+
if (pid) parent.set(pid, ppid);
|
|
236
|
+
}
|
|
237
|
+
const set = /* @__PURE__ */ new Set();
|
|
238
|
+
let cur = process.pid;
|
|
239
|
+
while (cur && cur > 1 && !set.has(cur)) {
|
|
240
|
+
set.add(cur);
|
|
241
|
+
cur = parent.get(cur);
|
|
242
|
+
}
|
|
243
|
+
return set;
|
|
244
|
+
}
|
|
245
|
+
function loadUsage() {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(fs.readFileSync(usageFile(), "utf-8"));
|
|
248
|
+
if (parsed?.version === 1 && parsed.apps) return parsed;
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
function saveUsage(data) {
|
|
254
|
+
try {
|
|
255
|
+
fs.mkdirSync(getConfigDir(), { recursive: true });
|
|
256
|
+
const tmp = usageFile() + ".tmp";
|
|
257
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
258
|
+
fs.renameSync(tmp, usageFile());
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function isAlive(pid) {
|
|
263
|
+
try {
|
|
264
|
+
process.kill(pid, 0);
|
|
265
|
+
return true;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
271
|
+
async function quitApp(app, force = false, timeoutMs = 1e4) {
|
|
272
|
+
const script = `ObjC.import('AppKit');
|
|
273
|
+
var a = $.NSRunningApplication.runningApplicationWithProcessIdentifier(${Math.trunc(app.pid)});
|
|
274
|
+
a.isNil() ? 'gone' : (a.terminate ? 'ok' : 'refused');`;
|
|
275
|
+
const out = (await run("osascript", ["-l", "JavaScript", "-e", script], 5e3)).trim();
|
|
276
|
+
if (out === "gone") return "quit";
|
|
277
|
+
const deadline = Date.now() + timeoutMs;
|
|
278
|
+
while (Date.now() < deadline) {
|
|
279
|
+
if (!isAlive(app.pid)) return "quit";
|
|
280
|
+
await sleep(250);
|
|
281
|
+
}
|
|
282
|
+
if (!force) return out === "ok" || out === "refused" ? "still-open" : "failed";
|
|
283
|
+
try {
|
|
284
|
+
process.kill(app.pid, "SIGTERM");
|
|
285
|
+
} catch {
|
|
286
|
+
return isAlive(app.pid) ? "failed" : "quit";
|
|
287
|
+
}
|
|
288
|
+
await sleep(3e3);
|
|
289
|
+
return isAlive(app.pid) ? "failed" : "forced";
|
|
290
|
+
}
|
|
291
|
+
async function ask(question) {
|
|
292
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
293
|
+
try {
|
|
294
|
+
return (await rl.question(question)).trim().toLowerCase();
|
|
295
|
+
} finally {
|
|
296
|
+
rl.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function confirm(question, yes) {
|
|
300
|
+
if (yes) return true;
|
|
301
|
+
if (!process.stdin.isTTY) return false;
|
|
302
|
+
const a = await ask(` ${question} ${chalk.dim("[y/N]")} `);
|
|
303
|
+
return a === "y" || a === "yes";
|
|
304
|
+
}
|
|
305
|
+
function notify(title, message) {
|
|
306
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
307
|
+
execFile("osascript", ["-e", `display notification "${esc(message)}" with title "${esc(title)}"`], () => {
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function pad(s, n) {
|
|
311
|
+
return s.length >= n ? s.slice(0, n - 1) + "\u2026" : s + " ".repeat(n - s.length);
|
|
312
|
+
}
|
|
313
|
+
function header(title) {
|
|
314
|
+
console.log(chalk.bold(`
|
|
315
|
+
\u{1F525} pyre optimize \u2014 ${title}
|
|
316
|
+
`));
|
|
317
|
+
}
|
|
318
|
+
function resolveSettings(opts) {
|
|
319
|
+
const cfg = readConfig();
|
|
320
|
+
const idleMs = parseDuration(opts.idle, parseDuration(cfg.optimizeIdleThreshold, 2 * 36e5));
|
|
321
|
+
if (isNaN(idleMs)) {
|
|
322
|
+
console.error(chalk.red(` \u2716 Invalid --idle value "${opts.idle}". Use e.g. 30m, 2h, 1d.`));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const keepRaw = opts.keep !== void 0 ? parseInt(opts.keep, 10) : cfg.optimizeKeepRecent;
|
|
326
|
+
const keep = Number.isFinite(keepRaw) && keepRaw >= 0 ? keepRaw : 5;
|
|
327
|
+
return { idleMs, keep, protectedApps: cfg.optimizeProtectedApps ?? [] };
|
|
328
|
+
}
|
|
329
|
+
async function analyzeApps(settings) {
|
|
330
|
+
const [apps, selfPids] = await Promise.all([listRunningApps(), getSelfPids()]);
|
|
331
|
+
const usage = loadUsage();
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const result = selectIdleApps(apps, {
|
|
334
|
+
now,
|
|
335
|
+
idleThresholdMs: settings.idleMs,
|
|
336
|
+
keepRecent: settings.keep,
|
|
337
|
+
protectedApps: settings.protectedApps,
|
|
338
|
+
selfPids,
|
|
339
|
+
usage
|
|
340
|
+
});
|
|
341
|
+
return { apps, usage, now, fresh: isTrackingFresh(usage, now), ...result };
|
|
342
|
+
}
|
|
343
|
+
function printCandidates(candidates, fresh) {
|
|
344
|
+
console.log(chalk.dim(` ${pad("App", 28)}${pad(fresh ? "Idle" : "Recency", 12)}${pad("RAM", 10)}CPU`));
|
|
345
|
+
for (const c of candidates) {
|
|
346
|
+
const when = c.idleMs !== void 0 ? formatDuration(c.idleMs) : `#${c.app.rank + 1}`;
|
|
347
|
+
console.log(` ${pad(c.app.name, 28)}${chalk.yellow(pad(when, 12))}${pad(formatKb(c.app.rssKb), 10)}${c.app.cpu.toFixed(1)}%`);
|
|
348
|
+
}
|
|
349
|
+
const total = candidates.reduce((s, c) => s + c.app.rssKb, 0);
|
|
350
|
+
console.log(chalk.dim(`
|
|
351
|
+
${candidates.length} app(s) using ~${formatKb(total)} RAM`));
|
|
352
|
+
}
|
|
353
|
+
function trackingHint(fresh) {
|
|
354
|
+
if (!fresh) {
|
|
355
|
+
console.log(chalk.dim(" \u2139 No live usage tracking. Ranking is based on activation order only."));
|
|
356
|
+
console.log(chalk.dim(" For real idle times run: ") + chalk.cyan("pyre optimize track --install") + "\n");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function runApps(opts) {
|
|
360
|
+
header("Idle Apps");
|
|
361
|
+
const settings = resolveSettings(opts);
|
|
362
|
+
const { candidates, skipped, fresh } = await analyzeApps(settings);
|
|
363
|
+
trackingHint(fresh);
|
|
364
|
+
console.log(chalk.dim(` Rules: idle \u2265 ${formatDuration(settings.idleMs)}, keep ${settings.keep} most recent, skip busy/protected apps
|
|
365
|
+
`));
|
|
366
|
+
if (skipped.length) {
|
|
367
|
+
console.log(chalk.dim(" Keeping:"));
|
|
368
|
+
for (const s of skipped) console.log(chalk.dim(` ${pad(s.app.name, 28)}${s.reason}`));
|
|
369
|
+
console.log();
|
|
370
|
+
}
|
|
371
|
+
if (!candidates.length) {
|
|
372
|
+
console.log(chalk.green(" \u2714 No idle apps to quit.\n"));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
console.log(chalk.bold(" Idle apps:"));
|
|
376
|
+
printCandidates(candidates, fresh);
|
|
377
|
+
console.log();
|
|
378
|
+
if (opts.dryRun) {
|
|
379
|
+
console.log(chalk.dim(" Dry run: nothing was quit.\n"));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let selected = [];
|
|
383
|
+
if (opts.yes) {
|
|
384
|
+
selected = candidates;
|
|
385
|
+
} else if (!process.stdin.isTTY) {
|
|
386
|
+
console.log(chalk.yellow(" Not an interactive terminal. Re-run with --yes to quit these apps.\n"));
|
|
387
|
+
return;
|
|
388
|
+
} else {
|
|
389
|
+
const a = await ask(` Quit these apps? ${chalk.dim("[a]ll / [s]elect / [N]one")} `);
|
|
390
|
+
if (a === "a" || a === "all" || a === "y" || a === "yes") {
|
|
391
|
+
selected = candidates;
|
|
392
|
+
} else if (a === "s" || a === "select") {
|
|
393
|
+
for (const c of candidates) {
|
|
394
|
+
if (await confirm(`Quit ${chalk.bold(c.app.name)} (${formatKb(c.app.rssKb)})?`)) selected.push(c);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!selected.length) {
|
|
399
|
+
console.log(chalk.dim(" Nothing quit.\n"));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
console.log();
|
|
403
|
+
let freed = 0;
|
|
404
|
+
for (const c of selected) {
|
|
405
|
+
process.stdout.write(` Quitting ${c.app.name}\u2026 `);
|
|
406
|
+
const res = await quitApp(c.app, !!opts.force);
|
|
407
|
+
if (res === "quit" || res === "forced") {
|
|
408
|
+
freed += c.app.rssKb;
|
|
409
|
+
console.log(res === "forced" ? chalk.yellow("force-quit") : chalk.green("\u2714"));
|
|
410
|
+
} else if (res === "still-open") {
|
|
411
|
+
console.log(chalk.yellow("still open (it may be waiting for you to save; use --force to terminate)"));
|
|
412
|
+
} else {
|
|
413
|
+
console.log(chalk.red("failed"));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
console.log(chalk.green(`
|
|
417
|
+
\u2714 Freed ~${formatKb(freed)} RAM
|
|
418
|
+
`));
|
|
419
|
+
}
|
|
420
|
+
async function frontApp() {
|
|
421
|
+
const asn = parseAsnList(await run("lsappinfo", ["front"], 3e3))[0];
|
|
422
|
+
if (!asn) return null;
|
|
423
|
+
const info = await run("lsappinfo", ["info", "-only", "bundleid", "-only", "name", asn], 3e3);
|
|
424
|
+
const bundleId = info.match(/"CFBundleIdentifier"="([^"]+)"/)?.[1];
|
|
425
|
+
const name = info.match(/"LSDisplayName"="([^"]+)"/)?.[1] ?? bundleId ?? "";
|
|
426
|
+
return bundleId ? { bundleId, name } : null;
|
|
427
|
+
}
|
|
428
|
+
async function runTracker(opts) {
|
|
429
|
+
if (opts.install) return installTracker(opts);
|
|
430
|
+
if (opts.uninstall) return uninstallTracker();
|
|
431
|
+
const autoQuitMs = opts.autoQuit ? parseDuration(opts.autoQuit, NaN) : void 0;
|
|
432
|
+
if (autoQuitMs !== void 0 && (isNaN(autoQuitMs) || autoQuitMs < 6e4)) {
|
|
433
|
+
console.error(chalk.red(` \u2716 Invalid --auto-quit value "${opts.autoQuit}". Use e.g. 2h (minimum 1m).`));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
let usage = loadUsage();
|
|
438
|
+
if (!usage) {
|
|
439
|
+
usage = { version: 1, trackingSince: now, updatedAt: now, apps: {} };
|
|
440
|
+
} else if (now - usage.updatedAt > TRACK_FRESH_MS) {
|
|
441
|
+
usage.trackingSince = now;
|
|
442
|
+
}
|
|
443
|
+
usage.updatedAt = now;
|
|
444
|
+
saveUsage(usage);
|
|
445
|
+
console.log(chalk.bold("\n \u{1F525} pyre optimize \u2014 usage tracker"));
|
|
446
|
+
console.log(chalk.dim(` Recording frontmost app every ${TRACK_INTERVAL_MS / 1e3}s \u2192 ${usageFile()}`));
|
|
447
|
+
if (autoQuitMs) console.log(chalk.yellow(` Auto-quit enabled: apps idle \u2265 ${formatDuration(autoQuitMs)} will be asked to quit`));
|
|
448
|
+
console.log(chalk.dim(" Press Ctrl+C to stop.\n"));
|
|
449
|
+
let lastFront = "";
|
|
450
|
+
let lastSave = 0;
|
|
451
|
+
let lastAutoQuit = 0;
|
|
452
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
453
|
+
const settings = resolveSettings({ ...opts, idle: opts.autoQuit });
|
|
454
|
+
const stop = () => {
|
|
455
|
+
saveUsage(usage);
|
|
456
|
+
process.exit(0);
|
|
457
|
+
};
|
|
458
|
+
process.on("SIGINT", stop);
|
|
459
|
+
process.on("SIGTERM", stop);
|
|
460
|
+
for (; ; ) {
|
|
461
|
+
const t = Date.now();
|
|
462
|
+
const front = await frontApp();
|
|
463
|
+
if (front) {
|
|
464
|
+
usage.apps[front.bundleId] = { name: front.name, lastActive: t };
|
|
465
|
+
}
|
|
466
|
+
usage.updatedAt = t;
|
|
467
|
+
const frontKey = front?.bundleId ?? "";
|
|
468
|
+
if (frontKey !== lastFront || t - lastSave > 3e4) {
|
|
469
|
+
saveUsage(usage);
|
|
470
|
+
lastSave = t;
|
|
471
|
+
if (frontKey !== lastFront && process.stdout.isTTY && front) {
|
|
472
|
+
console.log(chalk.dim(` ${new Date(t).toLocaleTimeString()} `) + front.name);
|
|
473
|
+
}
|
|
474
|
+
lastFront = frontKey;
|
|
475
|
+
}
|
|
476
|
+
if (autoQuitMs && t - lastAutoQuit > 6e4) {
|
|
477
|
+
lastAutoQuit = t;
|
|
478
|
+
try {
|
|
479
|
+
const { candidates } = await analyzeApps(settings);
|
|
480
|
+
const quitNames = [];
|
|
481
|
+
for (const c of candidates) {
|
|
482
|
+
if (c.idleMs === void 0 || attempted.has(c.app.pid)) continue;
|
|
483
|
+
attempted.add(c.app.pid);
|
|
484
|
+
const res = await quitApp(c.app, false, 8e3);
|
|
485
|
+
if (res === "quit") quitNames.push(c.app.name);
|
|
486
|
+
console.log(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} auto-quit ${c.app.name} (idle ${formatDuration(c.idleMs)}): ${res}`);
|
|
487
|
+
}
|
|
488
|
+
if (quitNames.length) {
|
|
489
|
+
notify("pyre optimize", `Quit ${quitNames.length} idle app(s): ${quitNames.join(", ")}`);
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
await sleep(TRACK_INTERVAL_MS);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function installTracker(opts) {
|
|
498
|
+
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
499
|
+
const args = [process.execPath, path.resolve(process.argv[1]), "optimize", "track", "--no-wizard"];
|
|
500
|
+
if (opts.autoQuit) {
|
|
501
|
+
if (isNaN(parseDuration(opts.autoQuit, NaN))) {
|
|
502
|
+
console.error(chalk.red(` \u2716 Invalid --auto-quit value "${opts.autoQuit}".`));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
args.push("--auto-quit", opts.autoQuit);
|
|
506
|
+
}
|
|
507
|
+
const logDir = getConfigDir();
|
|
508
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
509
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
510
|
+
<plist version="1.0">
|
|
511
|
+
<dict>
|
|
512
|
+
<key>Label</key>
|
|
513
|
+
<string>${TRACKER_LABEL}</string>
|
|
514
|
+
<key>ProgramArguments</key>
|
|
515
|
+
<array>
|
|
516
|
+
${args.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
|
|
517
|
+
</array>
|
|
518
|
+
<key>RunAtLoad</key>
|
|
519
|
+
<true/>
|
|
520
|
+
<key>KeepAlive</key>
|
|
521
|
+
<true/>
|
|
522
|
+
<key>ProcessType</key>
|
|
523
|
+
<string>Background</string>
|
|
524
|
+
<key>StandardOutPath</key>
|
|
525
|
+
<string>${xml(path.join(logDir, "optimize-tracker.log"))}</string>
|
|
526
|
+
<key>StandardErrorPath</key>
|
|
527
|
+
<string>${xml(path.join(logDir, "optimize-tracker.err"))}</string>
|
|
528
|
+
</dict>
|
|
529
|
+
</plist>
|
|
530
|
+
`;
|
|
531
|
+
try {
|
|
532
|
+
fs.mkdirSync(path.dirname(TRACKER_PLIST), { recursive: true });
|
|
533
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
534
|
+
fs.writeFileSync(TRACKER_PLIST, plist);
|
|
535
|
+
spawnSyncQuiet("launchctl", ["unload", TRACKER_PLIST]);
|
|
536
|
+
spawnSyncQuiet("launchctl", ["load", TRACKER_PLIST]);
|
|
537
|
+
console.log(chalk.green(`
|
|
538
|
+
\u2714 Usage tracker installed and started (${TRACKER_LABEL})`));
|
|
539
|
+
if (opts.autoQuit) console.log(chalk.yellow(` Auto-quit: apps idle \u2265 ${opts.autoQuit} will be asked to quit`));
|
|
540
|
+
console.log(chalk.dim(` Plist: ${TRACKER_PLIST}`));
|
|
541
|
+
console.log(chalk.dim(" Remove with: pyre optimize track --uninstall\n"));
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.log(chalk.red(` \u2716 Failed to install tracker: ${err.message}`));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function uninstallTracker() {
|
|
547
|
+
if (!fs.existsSync(TRACKER_PLIST)) {
|
|
548
|
+
console.log(chalk.yellow(`
|
|
549
|
+
No tracker installed at ${TRACKER_PLIST}
|
|
550
|
+
`));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
spawnSyncQuiet("launchctl", ["unload", TRACKER_PLIST]);
|
|
554
|
+
fs.unlinkSync(TRACKER_PLIST);
|
|
555
|
+
console.log(chalk.green(`
|
|
556
|
+
\u2714 Usage tracker uninstalled
|
|
557
|
+
`));
|
|
558
|
+
}
|
|
559
|
+
function spawnSyncQuiet(cmd, args) {
|
|
560
|
+
try {
|
|
561
|
+
spawnSync(cmd, args, { stdio: "ignore" });
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function collectMemory() {
|
|
566
|
+
const [pressure, swap, vm, ps] = await Promise.all([
|
|
567
|
+
run("memory_pressure", ["-Q"], 5e3),
|
|
568
|
+
run("sysctl", ["-n", "vm.swapusage"]),
|
|
569
|
+
run("vm_stat", []),
|
|
570
|
+
run("ps", ["-axo", "rss=,pid=,comm="])
|
|
571
|
+
]);
|
|
572
|
+
const freePct = pressure.match(/free percentage:\s*(\d+)%/i)?.[1];
|
|
573
|
+
const swapTotal = swap.match(/total = ([\d.]+)M/)?.[1];
|
|
574
|
+
const swapUsed = swap.match(/used = ([\d.]+)M/)?.[1];
|
|
575
|
+
const pageSize = parseInt(vm.match(/page size of (\d+) bytes/)?.[1] ?? "16384", 10);
|
|
576
|
+
const compressedPages = vm.match(/Pages occupied by compressor:\s+(\d+)/)?.[1];
|
|
577
|
+
const top = ps.split("\n").map((l) => l.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/)).filter((m) => !!m).map((m) => ({ rssKb: parseInt(m[1], 10), pid: parseInt(m[2], 10), name: path.basename(m[3]) })).sort((a, b) => b.rssKb - a.rssKb).slice(0, 10);
|
|
578
|
+
return {
|
|
579
|
+
totalBytes: os.totalmem(),
|
|
580
|
+
freePct: freePct ? parseInt(freePct, 10) : void 0,
|
|
581
|
+
swapUsedMb: swapUsed ? parseFloat(swapUsed) : void 0,
|
|
582
|
+
swapTotalMb: swapTotal ? parseFloat(swapTotal) : void 0,
|
|
583
|
+
compressedBytes: compressedPages ? parseInt(compressedPages, 10) * pageSize : void 0,
|
|
584
|
+
top
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function pressureLabel(freePct) {
|
|
588
|
+
if (freePct === void 0) return chalk.dim("unknown");
|
|
589
|
+
if (freePct >= 50) return chalk.green(`normal (${freePct}% free)`);
|
|
590
|
+
if (freePct >= 25) return chalk.yellow(`elevated (${freePct}% free)`);
|
|
591
|
+
return chalk.red(`high (${freePct}% free)`);
|
|
592
|
+
}
|
|
593
|
+
function printMemorySummary(m) {
|
|
594
|
+
console.log(` Memory pressure: ${pressureLabel(m.freePct)}`);
|
|
595
|
+
if (m.swapUsedMb !== void 0) {
|
|
596
|
+
const c = m.swapUsedMb > 2048 ? chalk.yellow : chalk.reset;
|
|
597
|
+
console.log(` Swap used: ${c(formatKb(m.swapUsedMb * 1024))}${m.swapTotalMb ? chalk.dim(` / ${formatKb(m.swapTotalMb * 1024)}`) : ""}`);
|
|
598
|
+
}
|
|
599
|
+
if (m.compressedBytes !== void 0) console.log(` Compressed: ${formatKb(m.compressedBytes / 1024)}`);
|
|
600
|
+
}
|
|
601
|
+
async function runMemory(opts) {
|
|
602
|
+
header("Memory");
|
|
603
|
+
const m = await collectMemory();
|
|
604
|
+
console.log(` Installed RAM: ${formatKb(m.totalBytes / 1024)}`);
|
|
605
|
+
printMemorySummary(m);
|
|
606
|
+
console.log(chalk.bold("\n Top memory users:"));
|
|
607
|
+
for (const p of m.top) console.log(` ${pad(p.name, 32)}${pad(formatKb(p.rssKb), 10)}${chalk.dim(`pid ${p.pid}`)}`);
|
|
608
|
+
console.log();
|
|
609
|
+
if (opts.dryRun) return;
|
|
610
|
+
if (await confirm("Run `sudo purge` to flush inactive file cache? (asks for your password)", opts.yes)) {
|
|
611
|
+
await runInherited("sudo", ["purge"]);
|
|
612
|
+
const after = await collectMemory();
|
|
613
|
+
console.log(`
|
|
614
|
+
After purge: ${pressureLabel(after.freePct)}
|
|
615
|
+
`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function runInherited(cmd, args) {
|
|
619
|
+
return new Promise((resolve) => {
|
|
620
|
+
const p = spawn(cmd, args, { stdio: "inherit" });
|
|
621
|
+
p.on("close", (code) => resolve(code ?? 1));
|
|
622
|
+
p.on("error", () => resolve(1));
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async function duKb(paths) {
|
|
626
|
+
const out = /* @__PURE__ */ new Map();
|
|
627
|
+
const existing = paths.filter((p) => fs.existsSync(p));
|
|
628
|
+
if (!existing.length) return out;
|
|
629
|
+
const raw = await run("du", ["-sk", ...existing], 12e4);
|
|
630
|
+
for (const line of raw.split("\n")) {
|
|
631
|
+
const m = line.match(/^(\d+)\s+(.+)$/);
|
|
632
|
+
if (m) out.set(m[2], parseInt(m[1], 10));
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
async function collectCaches() {
|
|
637
|
+
const home = os.homedir();
|
|
638
|
+
const libCaches = path.join(home, "Library", "Caches");
|
|
639
|
+
const known = [
|
|
640
|
+
{ name: "Xcode DerivedData", dir: path.join(home, "Library", "Developer", "Xcode", "DerivedData") },
|
|
641
|
+
{ name: "Homebrew downloads", dir: path.join(libCaches, "Homebrew") },
|
|
642
|
+
{ name: "npm cache", dir: path.join(home, ".npm", "_cacache") },
|
|
643
|
+
{ name: "Yarn cache", dir: path.join(libCaches, "Yarn") },
|
|
644
|
+
{ name: "pnpm store", dir: path.join(home, "Library", "pnpm", "store") },
|
|
645
|
+
{ name: "pip cache", dir: path.join(libCaches, "pip") }
|
|
646
|
+
];
|
|
647
|
+
const knownDirs = new Set(known.map((k) => k.dir));
|
|
648
|
+
const running = new Set((await listRunningApps()).map((a) => a.bundleId.toLowerCase()).filter(Boolean));
|
|
649
|
+
let otherChildren = [];
|
|
650
|
+
let skippedRunning = 0;
|
|
651
|
+
try {
|
|
652
|
+
otherChildren = fs.readdirSync(libCaches).map((n) => path.join(libCaches, n)).filter((p) => {
|
|
653
|
+
const n = path.basename(p).toLowerCase();
|
|
654
|
+
if (knownDirs.has(p) || n.startsWith("com.apple.") || n.startsWith(".")) return false;
|
|
655
|
+
if (running.has(n)) {
|
|
656
|
+
skippedRunning++;
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
660
|
+
});
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
const sizes = await duKb([...known.map((k) => k.dir), ...otherChildren]);
|
|
664
|
+
const cats = known.map((k) => ({ ...k, sizeKb: sizes.get(k.dir) ?? 0 }));
|
|
665
|
+
cats.push({
|
|
666
|
+
name: "Other app caches",
|
|
667
|
+
dir: libCaches,
|
|
668
|
+
children: otherChildren,
|
|
669
|
+
sizeKb: otherChildren.reduce((s, c) => s + (sizes.get(c) ?? 0), 0),
|
|
670
|
+
note: skippedRunning ? `${skippedRunning} cache(s) of running apps skipped` : void 0
|
|
671
|
+
});
|
|
672
|
+
return cats;
|
|
673
|
+
}
|
|
674
|
+
function removeContents(cat) {
|
|
675
|
+
let failed = 0;
|
|
676
|
+
let targets = cat.children ?? [];
|
|
677
|
+
if (!cat.children) {
|
|
678
|
+
try {
|
|
679
|
+
targets = fs.readdirSync(cat.dir).map((n) => path.join(cat.dir, n));
|
|
680
|
+
} catch {
|
|
681
|
+
return 1;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const t of targets) {
|
|
685
|
+
try {
|
|
686
|
+
fs.rmSync(t, { recursive: true, force: true });
|
|
687
|
+
} catch {
|
|
688
|
+
failed++;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return failed;
|
|
692
|
+
}
|
|
693
|
+
async function runCaches(opts) {
|
|
694
|
+
header("Caches");
|
|
695
|
+
process.stdout.write(chalk.dim(" Measuring cache sizes\u2026\r"));
|
|
696
|
+
const cats = await collectCaches();
|
|
697
|
+
const total = cats.reduce((s, c) => s + c.sizeKb, 0);
|
|
698
|
+
for (const c of cats) {
|
|
699
|
+
const size = c.sizeKb > 1024 * 1024 ? chalk.yellow(formatKb(c.sizeKb)) : formatKb(c.sizeKb);
|
|
700
|
+
console.log(` ${pad(c.name, 24)}${pad(size, 12)}${chalk.dim(c.note ?? "")}`);
|
|
701
|
+
}
|
|
702
|
+
console.log(chalk.bold(`
|
|
703
|
+
Total: ${formatKb(total)}
|
|
704
|
+
`));
|
|
705
|
+
if (!opts.clean) {
|
|
706
|
+
console.log(chalk.dim(" To delete caches: ") + chalk.cyan("pyre optimize caches --clean") + "\n");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (opts.dryRun) {
|
|
710
|
+
console.log(chalk.dim(" Dry run: nothing was deleted.\n"));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
714
|
+
console.log(chalk.yellow(" Not an interactive terminal. Re-run with --yes to delete.\n"));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
console.log(chalk.dim(" Tip: quit the related apps first. Caches are rebuilt automatically when needed.\n"));
|
|
718
|
+
let freed = 0;
|
|
719
|
+
for (const c of cats) {
|
|
720
|
+
if (c.sizeKb < 1024) continue;
|
|
721
|
+
if (!await confirm(`Delete ${c.name} (${formatKb(c.sizeKb)})?`, opts.yes)) continue;
|
|
722
|
+
const failed = removeContents(c);
|
|
723
|
+
const after = c.children ? 0 : (await duKb([c.dir])).get(c.dir) ?? 0;
|
|
724
|
+
freed += Math.max(0, c.sizeKb - after);
|
|
725
|
+
console.log(failed ? chalk.yellow(` \u2714 cleaned (${failed} item(s) could not be removed)`) : chalk.green(" \u2714 cleaned"));
|
|
726
|
+
}
|
|
727
|
+
console.log(chalk.green(`
|
|
728
|
+
\u2714 Freed ~${formatKb(freed)}
|
|
729
|
+
`));
|
|
730
|
+
}
|
|
731
|
+
async function trashSizeKb() {
|
|
732
|
+
const trash = path.join(os.homedir(), ".Trash");
|
|
733
|
+
try {
|
|
734
|
+
fs.readdirSync(trash);
|
|
735
|
+
} catch {
|
|
736
|
+
return void 0;
|
|
737
|
+
}
|
|
738
|
+
return (await duKb([trash])).get(trash) ?? 0;
|
|
739
|
+
}
|
|
740
|
+
async function runTrash(opts) {
|
|
741
|
+
header("Trash");
|
|
742
|
+
const size = await trashSizeKb();
|
|
743
|
+
console.log(` Trash size: ${size === void 0 ? chalk.dim("unknown (grant your terminal Full Disk Access to measure)") : formatKb(size)}
|
|
744
|
+
`);
|
|
745
|
+
if (!opts.empty) {
|
|
746
|
+
console.log(chalk.dim(" To empty: ") + chalk.cyan("pyre optimize trash --empty") + "\n");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (size === 0) {
|
|
750
|
+
console.log(chalk.green(" \u2714 Trash is already empty.\n"));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (opts.dryRun) {
|
|
754
|
+
console.log(chalk.dim(" Dry run: nothing was deleted.\n"));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!await confirm("Permanently delete everything in the Trash?", opts.yes)) return;
|
|
758
|
+
const trash = path.join(os.homedir(), ".Trash");
|
|
759
|
+
let ok = false;
|
|
760
|
+
if (size !== void 0) {
|
|
761
|
+
let failed = 0;
|
|
762
|
+
for (const n of fs.readdirSync(trash)) {
|
|
763
|
+
try {
|
|
764
|
+
fs.rmSync(path.join(trash, n), { recursive: true, force: true });
|
|
765
|
+
} catch {
|
|
766
|
+
failed++;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
ok = failed === 0;
|
|
770
|
+
}
|
|
771
|
+
if (!ok) {
|
|
772
|
+
ok = await runInherited("osascript", ["-e", 'tell application "Finder" to empty trash']) === 0;
|
|
773
|
+
}
|
|
774
|
+
console.log(ok ? chalk.green("\n \u2714 Trash emptied\n") : chalk.red("\n \u2716 Could not empty the Trash\n"));
|
|
775
|
+
}
|
|
776
|
+
async function collectLaunchItems() {
|
|
777
|
+
const dirs = [
|
|
778
|
+
[path.join(os.homedir(), "Library", "LaunchAgents"), "user agent"],
|
|
779
|
+
["/Library/LaunchAgents", "global agent"],
|
|
780
|
+
["/Library/LaunchDaemons", "daemon"]
|
|
781
|
+
];
|
|
782
|
+
const loadedRaw = await run("launchctl", ["list"]);
|
|
783
|
+
const loaded = new Set(loadedRaw.split("\n").slice(1).map((l) => l.trim().split(/\s+/)[2]).filter(Boolean));
|
|
784
|
+
const jobs = [];
|
|
785
|
+
for (const [dir, scope] of dirs) {
|
|
786
|
+
let files = [];
|
|
787
|
+
try {
|
|
788
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".plist"));
|
|
789
|
+
} catch {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
for (const f of files) {
|
|
793
|
+
const file = path.join(dir, f);
|
|
794
|
+
jobs.push((async () => {
|
|
795
|
+
const raw = await run("plutil", ["-convert", "json", "-o", "-", file], 3e3);
|
|
796
|
+
let j = {};
|
|
797
|
+
try {
|
|
798
|
+
j = JSON.parse(raw);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
const label = j.Label ?? f.replace(/\.plist$/, "");
|
|
802
|
+
const program = j.Program ?? (Array.isArray(j.ProgramArguments) ? j.ProgramArguments[0] : void 0);
|
|
803
|
+
const missing = !!program && path.isAbsolute(program) && !fs.existsSync(program);
|
|
804
|
+
return { label, file, scope, program, missing, disabled: j.Disabled === true, loaded: loaded.has(label) };
|
|
805
|
+
})());
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return (await Promise.all(jobs)).filter((x) => !!x);
|
|
809
|
+
}
|
|
810
|
+
async function runStartup() {
|
|
811
|
+
header("Startup Items");
|
|
812
|
+
const loginRaw = await run("osascript", ["-e", 'tell application "System Events" to get the name of every login item'], 8e3);
|
|
813
|
+
const loginItems = loginRaw.trim() ? loginRaw.trim().split(/,\s*/) : [];
|
|
814
|
+
console.log(chalk.bold(" Login items:"));
|
|
815
|
+
if (loginItems.length) for (const li of loginItems) console.log(` \u2022 ${li}`);
|
|
816
|
+
else console.log(chalk.dim(" none found (or System Events access was denied)"));
|
|
817
|
+
const items = await collectLaunchItems();
|
|
818
|
+
console.log(chalk.bold(`
|
|
819
|
+
Launch agents & daemons (${items.length}):`));
|
|
820
|
+
console.log(chalk.dim(` ${pad("Label", 52)}${pad("Scope", 14)}Status`));
|
|
821
|
+
for (const it of items.sort((a, b) => a.scope.localeCompare(b.scope) || a.label.localeCompare(b.label))) {
|
|
822
|
+
const status = it.missing ? chalk.red("broken: program missing") : it.disabled ? chalk.dim("disabled") : it.loaded ? chalk.green("loaded") : chalk.dim("not loaded");
|
|
823
|
+
console.log(` ${pad(it.label, 52)}${pad(it.scope, 14)}${status}`);
|
|
824
|
+
}
|
|
825
|
+
const broken = items.filter((i) => i.missing);
|
|
826
|
+
if (broken.length) {
|
|
827
|
+
console.log(chalk.yellow(`
|
|
828
|
+
\u26A0 ${broken.length} item(s) point to programs that no longer exist (leftovers from uninstalled apps):`));
|
|
829
|
+
for (const b of broken) console.log(chalk.dim(` ${b.file}`));
|
|
830
|
+
console.log(chalk.dim(" Review and delete these plist files manually if you no longer need them."));
|
|
831
|
+
}
|
|
832
|
+
console.log(chalk.dim("\n Manage login items in System Settings \u2192 General \u2192 Login Items.\n"));
|
|
833
|
+
}
|
|
834
|
+
async function runDns(opts) {
|
|
835
|
+
header("DNS Cache");
|
|
836
|
+
if (opts.dryRun) {
|
|
837
|
+
console.log(chalk.dim(" Dry run: would run `sudo dscacheutil -flushcache` and `sudo killall -HUP mDNSResponder`.\n"));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!await confirm("Flush the DNS cache? (asks for your password)", opts.yes)) return;
|
|
841
|
+
const a = await runInherited("sudo", ["dscacheutil", "-flushcache"]);
|
|
842
|
+
const b = a === 0 ? await runInherited("sudo", ["killall", "-HUP", "mDNSResponder"]) : 1;
|
|
843
|
+
console.log(a === 0 && b === 0 ? chalk.green("\n \u2714 DNS cache flushed\n") : chalk.red("\n \u2716 Failed to flush DNS cache\n"));
|
|
844
|
+
}
|
|
845
|
+
function runProtect(add, names) {
|
|
846
|
+
const cfg = readConfig();
|
|
847
|
+
const list = new Set(cfg.optimizeProtectedApps ?? []);
|
|
848
|
+
if (!names.length) {
|
|
849
|
+
console.log(chalk.bold("\n Protected apps (never quit):\n"));
|
|
850
|
+
for (const b of BUILTIN_PROTECTED) console.log(chalk.dim(` ${b} (built-in)`));
|
|
851
|
+
for (const p of list) console.log(` ${p}`);
|
|
852
|
+
console.log(chalk.dim('\n Add with: pyre optimize protect "<App Name or bundle id>"\n'));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
for (const n of names) {
|
|
856
|
+
if (add) list.add(n);
|
|
857
|
+
else {
|
|
858
|
+
for (const existing of [...list]) if (existing.toLowerCase() === n.toLowerCase()) list.delete(existing);
|
|
859
|
+
if (BUILTIN_PROTECTED.some((b) => b.toLowerCase() === n.toLowerCase())) {
|
|
860
|
+
console.log(chalk.yellow(` \u26A0 ${n} is built-in protected and can't be removed.`));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
writeConfig({ optimizeProtectedApps: [...list] });
|
|
865
|
+
console.log(chalk.green(` \u2714 ${add ? "Protected" : "Unprotected"}: ${names.join(", ")}`));
|
|
866
|
+
}
|
|
867
|
+
async function runOverview(opts) {
|
|
868
|
+
header("Overview");
|
|
869
|
+
const settings = resolveSettings(opts);
|
|
870
|
+
const [analysis, mem, trash, launch] = await Promise.all([
|
|
871
|
+
analyzeApps(settings),
|
|
872
|
+
collectMemory(),
|
|
873
|
+
trashSizeKb(),
|
|
874
|
+
collectLaunchItems()
|
|
875
|
+
]);
|
|
876
|
+
const { candidates, fresh, apps } = analysis;
|
|
877
|
+
console.log(chalk.bold(" Apps"));
|
|
878
|
+
console.log(` Open apps: ${apps.length}`);
|
|
879
|
+
console.log(` Usage tracker: ${fresh ? chalk.green("running") : fs.existsSync(TRACKER_PLIST) ? chalk.yellow("installed, not reporting") : chalk.dim("off")}`);
|
|
880
|
+
if (candidates.length) {
|
|
881
|
+
const ram = candidates.reduce((s, c) => s + c.app.rssKb, 0);
|
|
882
|
+
console.log(` Idle apps: ${chalk.yellow(String(candidates.length))} ${chalk.dim(`(~${formatKb(ram)} RAM)`)}: ${candidates.map((c) => c.app.name).join(", ")}`);
|
|
883
|
+
} else {
|
|
884
|
+
console.log(` Idle apps: ${chalk.green("none")}`);
|
|
885
|
+
}
|
|
886
|
+
console.log(chalk.bold("\n Memory"));
|
|
887
|
+
printMemorySummary(mem);
|
|
888
|
+
console.log(chalk.bold("\n Storage"));
|
|
889
|
+
console.log(` Trash: ${trash === void 0 ? chalk.dim("unknown") : formatKb(trash)}`);
|
|
890
|
+
console.log(chalk.dim(" Caches: run `pyre optimize caches` to measure"));
|
|
891
|
+
const broken = launch.filter((i) => i.missing).length;
|
|
892
|
+
console.log(chalk.bold("\n Startup"));
|
|
893
|
+
console.log(` Launch items: ${launch.length}${broken ? chalk.yellow(` (${broken} broken)`) : ""}`);
|
|
894
|
+
const tips = [];
|
|
895
|
+
if (candidates.length) tips.push(`${chalk.cyan("pyre optimize apps")} quit ${candidates.length} idle app(s)`);
|
|
896
|
+
if (!fresh) tips.push(`${chalk.cyan("pyre optimize track --install")} track real app idle times`);
|
|
897
|
+
if (mem.freePct !== void 0 && mem.freePct < 25) tips.push(`${chalk.cyan("pyre optimize memory")} memory pressure is high`);
|
|
898
|
+
if (trash !== void 0 && trash > 1024 * 1024) tips.push(`${chalk.cyan("pyre optimize trash --empty")} free ${formatKb(trash)}`);
|
|
899
|
+
if (broken) tips.push(`${chalk.cyan("pyre optimize startup")} review ${broken} broken launch item(s)`);
|
|
900
|
+
tips.push(`${chalk.cyan("pyre optimize caches")} see reclaimable cache space`);
|
|
901
|
+
console.log(chalk.bold("\n Suggestions"));
|
|
902
|
+
for (const t of tips) console.log(` \u2022 ${t}`);
|
|
903
|
+
console.log();
|
|
904
|
+
}
|
|
905
|
+
var USAGE = `
|
|
906
|
+
Usage: pyre optimize [subcommand] [options]
|
|
907
|
+
|
|
908
|
+
Subcommands:
|
|
909
|
+
(none) Overview report with suggestions (changes nothing)
|
|
910
|
+
apps Quit apps that are open but haven't been used recently
|
|
911
|
+
track Record app usage for real idle times (--install / --uninstall / --auto-quit <dur>)
|
|
912
|
+
memory Memory pressure, swap, top RAM users, optional purge
|
|
913
|
+
caches Measure caches (--clean to delete after confirmation)
|
|
914
|
+
trash Trash size (--empty to empty after confirmation)
|
|
915
|
+
startup Login items and launch agents, flags broken leftovers
|
|
916
|
+
dns Flush the DNS cache
|
|
917
|
+
protect [app] List or add apps that are never quit
|
|
918
|
+
unprotect <app> Remove an app from the protected list
|
|
919
|
+
|
|
920
|
+
Options:
|
|
921
|
+
--idle <dur> Idle threshold, e.g. 30m, 2h (default from config: 2h)
|
|
922
|
+
--keep <n> Always keep the N most recently used apps (default 5)
|
|
923
|
+
--dry-run Show what would happen without changing anything
|
|
924
|
+
-y, --yes Don't ask for confirmation
|
|
925
|
+
--force Terminate apps that don't quit within 10s
|
|
926
|
+
`;
|
|
927
|
+
async function runWindowsDns() {
|
|
928
|
+
console.log(chalk.bold("\n pyre optimize dns \u2014 Flush DNS Resolver Cache\n"));
|
|
929
|
+
try {
|
|
930
|
+
const { execSync } = await import("child_process");
|
|
931
|
+
execSync("ipconfig /flushdns", { stdio: "inherit" });
|
|
932
|
+
console.log(chalk.green("\n \u2714 Windows DNS Resolver Cache flushed successfully.\n"));
|
|
933
|
+
} catch (err) {
|
|
934
|
+
console.log(chalk.red(`
|
|
935
|
+
\u2716 Failed to flush DNS cache: ${err.message}
|
|
936
|
+
`));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function runWindowsTrash(opts) {
|
|
940
|
+
console.log(chalk.bold("\n pyre optimize trash \u2014 Windows Recycle Bin\n"));
|
|
941
|
+
if (opts.empty || opts.yes) {
|
|
942
|
+
try {
|
|
943
|
+
const { runPowerShell } = await import("./run-UJQYT2TO.js");
|
|
944
|
+
await runPowerShell("Clear-RecycleBin -Force -ErrorAction SilentlyContinue", "");
|
|
945
|
+
console.log(chalk.green(" \u2714 Emptied Recycle Bin.\n"));
|
|
946
|
+
} catch (err) {
|
|
947
|
+
console.log(chalk.red(` \u2716 Could not empty Recycle Bin: ${err.message}
|
|
948
|
+
`));
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
console.log(chalk.cyan(" Pass --empty or -y to empty the Recycle Bin.\n"));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function runWindowsCaches(opts) {
|
|
955
|
+
console.log(chalk.bold("\n pyre optimize caches \u2014 Windows Temporary Files\n"));
|
|
956
|
+
const tempDir = os.tmpdir();
|
|
957
|
+
let totalBytes = 0;
|
|
958
|
+
let fileCount = 0;
|
|
959
|
+
try {
|
|
960
|
+
if (fs.existsSync(tempDir)) {
|
|
961
|
+
const files = fs.readdirSync(tempDir);
|
|
962
|
+
for (const f of files) {
|
|
963
|
+
try {
|
|
964
|
+
const stat = fs.statSync(path.join(tempDir, f));
|
|
965
|
+
if (stat.isFile()) {
|
|
966
|
+
totalBytes += stat.size;
|
|
967
|
+
fileCount++;
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
const mb = Math.round(totalBytes / (1024 * 1024) * 10) / 10;
|
|
976
|
+
console.log(` Temporary files: ${chalk.bold(String(fileCount))} files (${chalk.yellow(`${mb} MB`)}) in ${chalk.dim(tempDir)}`);
|
|
977
|
+
if (opts.clean || opts.yes) {
|
|
978
|
+
let deleted = 0;
|
|
979
|
+
try {
|
|
980
|
+
const files = fs.readdirSync(tempDir);
|
|
981
|
+
for (const f of files) {
|
|
982
|
+
try {
|
|
983
|
+
const full = path.join(tempDir, f);
|
|
984
|
+
const stat = fs.statSync(full);
|
|
985
|
+
if (stat.isFile()) {
|
|
986
|
+
fs.unlinkSync(full);
|
|
987
|
+
deleted++;
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
console.log(chalk.green(` \u2714 Cleaned ${deleted} temporary cache files.
|
|
995
|
+
`));
|
|
996
|
+
} else {
|
|
997
|
+
console.log(chalk.dim(" Run with --clean or -y to remove temporary cache files.\n"));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function runWindowsStartup() {
|
|
1001
|
+
console.log(chalk.bold("\n pyre optimize startup \u2014 Windows Startup Applications\n"));
|
|
1002
|
+
try {
|
|
1003
|
+
const { runPowerShell } = await import("./run-UJQYT2TO.js");
|
|
1004
|
+
const raw = await runPowerShell(
|
|
1005
|
+
`$hkcu = Get-ItemProperty 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -ErrorAction SilentlyContinue; $hklm = Get-ItemProperty 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -ErrorAction SilentlyContinue; if ($hkcu) { $hkcu.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { "User|$($_.Name)|$($_.Value)" } }; if ($hklm) { $hklm.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { "System|$($_.Name)|$($_.Value)" } }`,
|
|
1006
|
+
""
|
|
1007
|
+
);
|
|
1008
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1009
|
+
if (lines.length === 0) {
|
|
1010
|
+
console.log(chalk.dim(" No registry startup entries found.\n"));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
for (const l of lines) {
|
|
1014
|
+
const [scope, name, val] = l.split("|");
|
|
1015
|
+
console.log(` \u2022 ${chalk.bold(name)} [${chalk.cyan(scope)}]`);
|
|
1016
|
+
console.log(` ${chalk.dim(val)}`);
|
|
1017
|
+
}
|
|
1018
|
+
console.log();
|
|
1019
|
+
} catch {
|
|
1020
|
+
console.log(chalk.dim(" Could not query startup entries.\n"));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function runOptimizeCommand(sub, args, opts) {
|
|
1024
|
+
if (process.platform === "win32") {
|
|
1025
|
+
switch (sub) {
|
|
1026
|
+
case "dns":
|
|
1027
|
+
return runWindowsDns();
|
|
1028
|
+
case "caches":
|
|
1029
|
+
case "cache":
|
|
1030
|
+
return runWindowsCaches(opts);
|
|
1031
|
+
case "trash":
|
|
1032
|
+
return runWindowsTrash(opts);
|
|
1033
|
+
case "startup":
|
|
1034
|
+
return runWindowsStartup();
|
|
1035
|
+
case "protect":
|
|
1036
|
+
return runProtect(true, args);
|
|
1037
|
+
case "unprotect":
|
|
1038
|
+
return runProtect(false, args);
|
|
1039
|
+
case "help":
|
|
1040
|
+
console.log(USAGE);
|
|
1041
|
+
return;
|
|
1042
|
+
case void 0:
|
|
1043
|
+
case "report":
|
|
1044
|
+
console.log(chalk.bold("\n pyre optimize \u2014 Windows Housekeeping\n"));
|
|
1045
|
+
console.log(" Available subcommands on Windows:");
|
|
1046
|
+
console.log(` ${chalk.cyan("pyre optimize dns")} Flush DNS Resolver Cache`);
|
|
1047
|
+
console.log(` ${chalk.cyan("pyre optimize caches")} Scan and clean temporary files (%TEMP%)`);
|
|
1048
|
+
console.log(` ${chalk.cyan("pyre optimize trash")} Inspect and empty Recycle Bin`);
|
|
1049
|
+
console.log(` ${chalk.cyan("pyre optimize startup")} Inspect Windows startup items
|
|
1050
|
+
`);
|
|
1051
|
+
return;
|
|
1052
|
+
default:
|
|
1053
|
+
console.log(chalk.yellow(` Subcommand "${sub}" requires macOS WindowServer APIs.`));
|
|
1054
|
+
console.log(" On Windows, use: dns, caches, trash, startup, protect, unprotect.\n");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (process.platform !== "darwin") {
|
|
1059
|
+
console.log(chalk.yellow(" pyre optimize is only available on macOS and Windows."));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
switch (sub) {
|
|
1063
|
+
case void 0:
|
|
1064
|
+
case "report":
|
|
1065
|
+
return runOverview(opts);
|
|
1066
|
+
case "apps":
|
|
1067
|
+
return runApps(opts);
|
|
1068
|
+
case "track":
|
|
1069
|
+
return runTracker(opts);
|
|
1070
|
+
case "memory":
|
|
1071
|
+
case "mem":
|
|
1072
|
+
return runMemory(opts);
|
|
1073
|
+
case "caches":
|
|
1074
|
+
case "cache":
|
|
1075
|
+
return runCaches(opts);
|
|
1076
|
+
case "trash":
|
|
1077
|
+
return runTrash(opts);
|
|
1078
|
+
case "startup":
|
|
1079
|
+
return runStartup();
|
|
1080
|
+
case "dns":
|
|
1081
|
+
return runDns(opts);
|
|
1082
|
+
case "protect":
|
|
1083
|
+
return runProtect(true, args);
|
|
1084
|
+
case "unprotect":
|
|
1085
|
+
return runProtect(false, args);
|
|
1086
|
+
case "help":
|
|
1087
|
+
console.log(USAGE);
|
|
1088
|
+
return;
|
|
1089
|
+
default:
|
|
1090
|
+
console.log(chalk.red(` \u2716 Unknown subcommand "${sub}"`));
|
|
1091
|
+
console.log(USAGE);
|
|
1092
|
+
process.exitCode = 1;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
export {
|
|
1096
|
+
BUILTIN_PROTECTED,
|
|
1097
|
+
computeIdleMs,
|
|
1098
|
+
formatDuration,
|
|
1099
|
+
formatKb,
|
|
1100
|
+
isProtected,
|
|
1101
|
+
isTrackingFresh,
|
|
1102
|
+
listRunningApps,
|
|
1103
|
+
loadUsage,
|
|
1104
|
+
parseAsnList,
|
|
1105
|
+
parseDuration,
|
|
1106
|
+
parseLsappinfoList,
|
|
1107
|
+
quitApp,
|
|
1108
|
+
runOptimizeCommand,
|
|
1109
|
+
selectIdleApps
|
|
1110
|
+
};
|