@indigoai-us/hq-cli 5.345.147 → 5.345.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +297 -3
- package/dist/command-catalog.generated.js +368 -3
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
- package/dist/commands/core-refresh-vault-access-native.js +214 -19
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +9 -2
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +7 -0
- package/dist/commands/install-global.js +129 -35
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +36 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
- package/dist/lib/doctor/checks/anywhere.js +23 -8
- package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
- package/dist/lib/doctor/checks/cli-path.js +45 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +16 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +10 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/user-path.d.ts +48 -0
- package/dist/lib/windows/user-path.js +114 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sweeper for abandoned throwaway `$HOME` folders.
|
|
3
|
+
*
|
|
4
|
+
* Commands run as `HOME=$(mktemp -d) <cmd>` leave a `tmp.XXXXXXXX` folder in
|
|
5
|
+
* the temp dir. When the command touched Rust or pnpm tooling, that folder
|
|
6
|
+
* holds a full `.rustup` / `.cargo` download (1-2 GB) and nothing deletes it.
|
|
7
|
+
* On 2026-10-18 about 45 GB refilled within hours of a cleanup this way.
|
|
8
|
+
* `core/scripts/with-temp-home.sh` makes `hq-temp-home.XXXXXXXX` folders and
|
|
9
|
+
* removes them on exit, but a wrapper killed with SIGKILL leaves its folder.
|
|
10
|
+
*
|
|
11
|
+
* The sweep is deliberately narrow. A folder is removed only when ALL hold:
|
|
12
|
+
*
|
|
13
|
+
* - it is a direct child of a temp root (TMPDIR, os.tmpdir(), /tmp,
|
|
14
|
+
* /private/tmp, /var/folders/{a}/{b}/T) and its name starts with `tmp.` or
|
|
15
|
+
* `hq-temp-home.`;
|
|
16
|
+
* - `lstat` says it is a real directory (never a symlink) owned by this user;
|
|
17
|
+
* - a `tmp.*` folder holds a `.rustup` or `.cargo` directory (again by
|
|
18
|
+
* `lstat`). An `hq-temp-home.*` folder needs no marker: the name shows the
|
|
19
|
+
* wrapper made it;
|
|
20
|
+
* - nothing inside it, the folder itself included, was modified in the last
|
|
21
|
+
* 90 minutes;
|
|
22
|
+
* - no live process uses it: the wrapper's `.hq-temp-home.lock` does not name
|
|
23
|
+
* a live pid, and no process of this user has its cwd or an open file
|
|
24
|
+
* there (on Linux, or its `HOME` there either).
|
|
25
|
+
*
|
|
26
|
+
* The walk uses `lstat` throughout and never descends into a symlink, so a
|
|
27
|
+
* link inside a temp home that points elsewhere is neither measured nor
|
|
28
|
+
* followed. `fs.rmSync` unlinks symlinks rather than following them. A folder
|
|
29
|
+
* that cannot be fully read is kept, because an incomplete scan cannot prove
|
|
30
|
+
* the folder is idle. When the in-use probe fails, every candidate is kept.
|
|
31
|
+
*
|
|
32
|
+
* `shallow` mode is for plain `hq doctor`, which must stay fast. It checks
|
|
33
|
+
* only the folder and its direct children for recent changes, skips the size
|
|
34
|
+
* walk and the process probe, and reports no byte counts.
|
|
35
|
+
*/
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import * as fs from "node:fs";
|
|
38
|
+
import * as os from "node:os";
|
|
39
|
+
import * as path from "node:path";
|
|
40
|
+
/** Folders younger than this (by newest mtime anywhere inside) are kept. */
|
|
41
|
+
export const TEMP_HOME_MIN_IDLE_MS = 90 * 60 * 1000;
|
|
42
|
+
/** Names that mark a `tmp.*` temp home as holding a toolchain download. */
|
|
43
|
+
export const TEMP_HOME_TOOLCHAIN_MARKERS = [".rustup", ".cargo"];
|
|
44
|
+
/** Folders made by `HOME=$(mktemp -d)`; swept only with a toolchain marker. */
|
|
45
|
+
export const TEMP_HOME_MKTEMP_PREFIX = "tmp.";
|
|
46
|
+
/** Folders made by `core/scripts/with-temp-home.sh`. */
|
|
47
|
+
export const TEMP_HOME_WRAPPER_PREFIX = "hq-temp-home.";
|
|
48
|
+
/** Lock file the wrapper writes into its temp home; holds the wrapper pid. */
|
|
49
|
+
export const TEMP_HOME_LOCK_FILE = ".hq-temp-home.lock";
|
|
50
|
+
const BYTES_PER_GB = 1_000_000_000;
|
|
51
|
+
const nodeFs = {
|
|
52
|
+
lstatSync: (target) => fs.lstatSync(target),
|
|
53
|
+
readdirSync: (target) => fs.readdirSync(target),
|
|
54
|
+
realpathSync: (target) => fs.realpathSync(target),
|
|
55
|
+
readFileSync: (target, encoding) => fs.readFileSync(target, encoding),
|
|
56
|
+
rmSync: (target, options) => fs.rmSync(target, options),
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The temp roots a throwaway home can land in: `$TMPDIR`, `os.tmpdir()`,
|
|
60
|
+
* `/tmp`, `/private/tmp`, and every per-user macOS temp dir
|
|
61
|
+
* `/var/folders/{a}/{b}/T`. Missing roots are dropped later by the sweep.
|
|
62
|
+
*/
|
|
63
|
+
export function defaultTempRoots(env = process.env, fsImpl = nodeFs) {
|
|
64
|
+
const roots = [];
|
|
65
|
+
if (env.TMPDIR)
|
|
66
|
+
roots.push(env.TMPDIR);
|
|
67
|
+
roots.push(os.tmpdir(), "/tmp", "/private/tmp");
|
|
68
|
+
for (const a of safeReaddir(fsImpl, "/var/folders")) {
|
|
69
|
+
for (const b of safeReaddir(fsImpl, path.join("/var/folders", a))) {
|
|
70
|
+
roots.push(path.join("/var/folders", a, b, "T"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return roots;
|
|
74
|
+
}
|
|
75
|
+
function safeReaddir(fsImpl, target) {
|
|
76
|
+
try {
|
|
77
|
+
return fsImpl.readdirSync(target);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function errorMessage(error) {
|
|
84
|
+
return error instanceof Error ? error.message : String(error);
|
|
85
|
+
}
|
|
86
|
+
/** `process.kill(pid, 0)`: alive unless the pid does not exist (ESRCH). */
|
|
87
|
+
export function defaultIsPidAlive(pid) {
|
|
88
|
+
try {
|
|
89
|
+
process.kill(pid, 0);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return error.code === "EPERM";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* True when `value` (a cwd or open-file path, or `HOME`) is `home` or a path
|
|
98
|
+
* inside it. Matched by folder name as a path segment, because the same temp
|
|
99
|
+
* dir is reachable as `/var/folders/...` and `/private/var/folders/...`; the
|
|
100
|
+
* random `mktemp` suffix makes the name unique. Over-matching only keeps more.
|
|
101
|
+
*/
|
|
102
|
+
export function pathUsesHome(value, home) {
|
|
103
|
+
const name = path.basename(home);
|
|
104
|
+
if (value === name)
|
|
105
|
+
return true;
|
|
106
|
+
const marker = `/${name}`;
|
|
107
|
+
let index = value.indexOf(marker);
|
|
108
|
+
while (index !== -1) {
|
|
109
|
+
const next = value[index + marker.length];
|
|
110
|
+
if (next === undefined || next === "/")
|
|
111
|
+
return true;
|
|
112
|
+
index = value.indexOf(marker, index + 1);
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function runProbe(command, args) {
|
|
117
|
+
const result = spawnSync(command, args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
118
|
+
if (result.error)
|
|
119
|
+
throw result.error;
|
|
120
|
+
if (result.status !== 0 && !result.stdout) {
|
|
121
|
+
throw new Error(`${command} exited ${result.status ?? result.signal}`);
|
|
122
|
+
}
|
|
123
|
+
return result.stdout;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Default in-use probe. Only processes of `uid` are checked; other users
|
|
127
|
+
* cannot own these folders' contents in a way the sweep would delete.
|
|
128
|
+
*
|
|
129
|
+
* - Linux: `/proc/{pid}/cwd`, every `/proc/{pid}/fd/*` link, and `HOME` from
|
|
130
|
+
* `/proc/{pid}/environ`. Environment values are matched in memory and never
|
|
131
|
+
* logged.
|
|
132
|
+
* - macOS: `lsof -u uid` for every file each process has open, its cwd and
|
|
133
|
+
* its executable included. macOS does not show other processes'
|
|
134
|
+
* environments (`ps -E` leaves them out), so `HOME` itself is not checked
|
|
135
|
+
* there. A process that has HOME set to a temp home but holds nothing open
|
|
136
|
+
* inside it is not seen; the wrapper lock and the 90-minute idle check
|
|
137
|
+
* cover that case.
|
|
138
|
+
*/
|
|
139
|
+
export function defaultInUseProbe(uid) {
|
|
140
|
+
return (homes) => {
|
|
141
|
+
const used = new Set();
|
|
142
|
+
if (homes.length === 0)
|
|
143
|
+
return used;
|
|
144
|
+
const values = [];
|
|
145
|
+
if (process.platform === "linux") {
|
|
146
|
+
for (const pid of fs.readdirSync("/proc").filter((name) => /^\d+$/.test(name))) {
|
|
147
|
+
try {
|
|
148
|
+
if (uid !== undefined && fs.statSync(`/proc/${pid}`).uid !== uid)
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
values.push(fs.readlinkSync(`/proc/${pid}/cwd`));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// The process exited or the link is unreadable.
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
for (const fd of fs.readdirSync(`/proc/${pid}/fd`)) {
|
|
162
|
+
try {
|
|
163
|
+
values.push(fs.readlinkSync(`/proc/${pid}/fd/${fd}`));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// The descriptor closed.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Same.
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
for (const entry of fs.readFileSync(`/proc/${pid}/environ`, "utf8").split("\0")) {
|
|
175
|
+
if (entry.startsWith("HOME="))
|
|
176
|
+
values.push(entry.slice(5));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Same.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const userArgs = uid !== undefined ? ["-u", String(uid)] : [];
|
|
186
|
+
// -w: no warnings; -n and -P: no host or port name lookups.
|
|
187
|
+
const lsof = runProbe("lsof", ["-w", "-n", "-P", "-Fn", ...userArgs]);
|
|
188
|
+
for (const line of lsof.split("\n")) {
|
|
189
|
+
if (line.startsWith("n"))
|
|
190
|
+
values.push(line.slice(1));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const home of homes) {
|
|
194
|
+
if (values.some((value) => pathUsesHome(value, home)))
|
|
195
|
+
used.add(home);
|
|
196
|
+
}
|
|
197
|
+
return used;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** On-disk size of one lstat result: allocated blocks when known, else size. */
|
|
201
|
+
function diskBytes(stat) {
|
|
202
|
+
return typeof stat.blocks === "number" && stat.blocks >= 0 ? stat.blocks * 512 : stat.size;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Measure a folder with `lstat`, never descending into symlinks. Stops as soon
|
|
206
|
+
* as it sees an entry modified after `cutoffMs`, since such a folder is kept
|
|
207
|
+
* and its size is not needed. With `maxDepth` 1 it reads only direct children.
|
|
208
|
+
*/
|
|
209
|
+
function walk(fsImpl, dir, dirStat, cutoffMs, maxDepth = Number.POSITIVE_INFINITY) {
|
|
210
|
+
if (dirStat.mtimeMs > cutoffMs)
|
|
211
|
+
return { complete: true, recent: true, bytes: 0 };
|
|
212
|
+
let bytes = diskBytes(dirStat);
|
|
213
|
+
const stack = [{ dir, depth: 0 }];
|
|
214
|
+
while (stack.length > 0) {
|
|
215
|
+
const current = stack.pop();
|
|
216
|
+
let names;
|
|
217
|
+
try {
|
|
218
|
+
names = fsImpl.readdirSync(current.dir);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return { complete: false, recent: false, bytes };
|
|
222
|
+
}
|
|
223
|
+
for (const name of names) {
|
|
224
|
+
const child = path.join(current.dir, name);
|
|
225
|
+
let stat;
|
|
226
|
+
try {
|
|
227
|
+
stat = fsImpl.lstatSync(child);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { complete: false, recent: false, bytes };
|
|
231
|
+
}
|
|
232
|
+
if (stat.mtimeMs > cutoffMs)
|
|
233
|
+
return { complete: true, recent: true, bytes };
|
|
234
|
+
bytes += diskBytes(stat);
|
|
235
|
+
if (stat.isDirectory() && !stat.isSymbolicLink() && current.depth + 1 < maxDepth) {
|
|
236
|
+
stack.push({ dir: child, depth: current.depth + 1 });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { complete: true, recent: false, bytes };
|
|
241
|
+
}
|
|
242
|
+
function hasToolchainMarker(fsImpl, dir) {
|
|
243
|
+
return TEMP_HOME_TOOLCHAIN_MARKERS.some((marker) => {
|
|
244
|
+
try {
|
|
245
|
+
const stat = fsImpl.lstatSync(path.join(dir, marker));
|
|
246
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** True when the wrapper's lock file in `dir` names a live pid. */
|
|
254
|
+
function lockHeldByLiveProcess(fsImpl, dir, isPidAlive) {
|
|
255
|
+
let text;
|
|
256
|
+
try {
|
|
257
|
+
const lock = path.join(dir, TEMP_HOME_LOCK_FILE);
|
|
258
|
+
if (!fsImpl.lstatSync(lock).isFile())
|
|
259
|
+
return false;
|
|
260
|
+
text = fsImpl.readFileSync(lock, "utf8");
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
266
|
+
return Number.isInteger(pid) && pid > 0 && isPidAlive(pid);
|
|
267
|
+
}
|
|
268
|
+
function resolveRoots(fsImpl, roots) {
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
const resolved = [];
|
|
271
|
+
for (const root of roots) {
|
|
272
|
+
let real;
|
|
273
|
+
try {
|
|
274
|
+
real = fsImpl.realpathSync(root);
|
|
275
|
+
if (!fsImpl.lstatSync(real).isDirectory())
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (seen.has(real))
|
|
282
|
+
continue;
|
|
283
|
+
seen.add(real);
|
|
284
|
+
resolved.push(real);
|
|
285
|
+
}
|
|
286
|
+
return resolved;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Find and (unless `dryRun`) remove idle, unused temp homes. See the module
|
|
290
|
+
* comment for the exact rules.
|
|
291
|
+
*/
|
|
292
|
+
export function sweepTempHomes(options = {}) {
|
|
293
|
+
const fsImpl = options.fs ?? nodeFs;
|
|
294
|
+
const shallow = options.shallow === true;
|
|
295
|
+
const dryRun = shallow || options.dryRun === true;
|
|
296
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
297
|
+
const cutoffMs = nowMs - (options.minIdleMs ?? TEMP_HOME_MIN_IDLE_MS);
|
|
298
|
+
const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
|
|
299
|
+
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
300
|
+
const roots = resolveRoots(fsImpl, options.roots ?? defaultTempRoots());
|
|
301
|
+
// Pass 1: decide per folder from the file system alone.
|
|
302
|
+
const entries = [];
|
|
303
|
+
const candidates = [];
|
|
304
|
+
for (const root of roots) {
|
|
305
|
+
const names = safeReaddir(fsImpl, root)
|
|
306
|
+
.filter((name) => name.startsWith(TEMP_HOME_MKTEMP_PREFIX) || name.startsWith(TEMP_HOME_WRAPPER_PREFIX))
|
|
307
|
+
.sort();
|
|
308
|
+
for (const name of names) {
|
|
309
|
+
const candidate = path.join(root, name);
|
|
310
|
+
let stat;
|
|
311
|
+
try {
|
|
312
|
+
stat = fsImpl.lstatSync(candidate);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
entries.push({ path: candidate, action: "kept", reason: "unreadable", error: errorMessage(error) });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (stat.isSymbolicLink()) {
|
|
319
|
+
entries.push({ path: candidate, action: "kept", reason: "symlink" });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!stat.isDirectory()) {
|
|
323
|
+
entries.push({ path: candidate, action: "kept", reason: "not-directory" });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
327
|
+
entries.push({ path: candidate, action: "kept", reason: "not-owned" });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (name.startsWith(TEMP_HOME_MKTEMP_PREFIX) && !hasToolchainMarker(fsImpl, candidate)) {
|
|
331
|
+
entries.push({ path: candidate, action: "kept", reason: "no-toolchain-marker" });
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const measured = walk(fsImpl, candidate, stat, cutoffMs, shallow ? 1 : undefined);
|
|
335
|
+
if (measured.recent) {
|
|
336
|
+
entries.push({ path: candidate, action: "kept", reason: "recent" });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!measured.complete) {
|
|
340
|
+
entries.push({ path: candidate, action: "kept", reason: "unreadable" });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (lockHeldByLiveProcess(fsImpl, candidate, isPidAlive)) {
|
|
344
|
+
entries.push({ path: candidate, action: "kept", reason: "in-use" });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
candidates.push({ path: candidate, stat, bytes: shallow ? undefined : measured.bytes });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Pass 2: one process probe for every remaining candidate. Shallow mode
|
|
351
|
+
// only reports, so it skips the probe; the full sweep runs it.
|
|
352
|
+
let inUse = new Set();
|
|
353
|
+
if (!shallow && candidates.length > 0) {
|
|
354
|
+
const probe = options.inUse ?? defaultInUseProbe(uid);
|
|
355
|
+
try {
|
|
356
|
+
inUse = probe(candidates.map((candidate) => candidate.path));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const message = errorMessage(error);
|
|
360
|
+
for (const candidate of candidates) {
|
|
361
|
+
entries.push({ path: candidate.path, action: "kept", reason: "in-use-unknown", error: message });
|
|
362
|
+
}
|
|
363
|
+
return finish(entries, roots, dryRun, shallow);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Pass 3: remove, or report what would be removed.
|
|
367
|
+
for (const candidate of candidates) {
|
|
368
|
+
if (inUse.has(candidate.path)) {
|
|
369
|
+
entries.push({ path: candidate.path, action: "kept", reason: "in-use" });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (dryRun) {
|
|
373
|
+
entries.push(withBytes({ path: candidate.path, action: "would-remove" }, candidate.bytes));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
// Re-check right before deleting: the path must still be the same real
|
|
377
|
+
// directory, untouched since it was measured, and not newly locked.
|
|
378
|
+
let again;
|
|
379
|
+
try {
|
|
380
|
+
again = fsImpl.lstatSync(candidate.path);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
entries.push({ path: candidate.path, action: "kept", reason: "changed-before-delete", error: errorMessage(error) });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (again.isSymbolicLink() ||
|
|
387
|
+
!again.isDirectory() ||
|
|
388
|
+
again.ino !== candidate.stat.ino ||
|
|
389
|
+
again.dev !== candidate.stat.dev ||
|
|
390
|
+
again.mtimeMs > cutoffMs ||
|
|
391
|
+
lockHeldByLiveProcess(fsImpl, candidate.path, isPidAlive)) {
|
|
392
|
+
entries.push({ path: candidate.path, action: "kept", reason: "changed-before-delete" });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
fsImpl.rmSync(candidate.path, { recursive: true, force: false });
|
|
397
|
+
entries.push(withBytes({ path: candidate.path, action: "removed" }, candidate.bytes));
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
entries.push(withBytes({ path: candidate.path, action: "failed", error: errorMessage(error) }, candidate.bytes));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return finish(entries, roots, dryRun, shallow);
|
|
404
|
+
}
|
|
405
|
+
function withBytes(entry, bytes) {
|
|
406
|
+
return bytes === undefined ? entry : { ...entry, bytes };
|
|
407
|
+
}
|
|
408
|
+
function finish(entries, roots, dryRun, shallow) {
|
|
409
|
+
const bytesFreed = entries
|
|
410
|
+
.filter((entry) => entry.action === "removed" || entry.action === "would-remove")
|
|
411
|
+
.reduce((sum, entry) => sum + (entry.bytes ?? 0), 0);
|
|
412
|
+
return { dryRun, shallow, roots, entries, bytesFreed, gbFreed: toGb(bytesFreed) };
|
|
413
|
+
}
|
|
414
|
+
/** Bytes to GB (10^9 bytes), rounded to two decimals. */
|
|
415
|
+
export function toGb(bytes) {
|
|
416
|
+
return Math.round((bytes / BYTES_PER_GB) * 100) / 100;
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=temp-home-sweeper.js.map
|