@bli-cockpit/cli 0.1.33 → 0.2.1
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 +119 -202
- package/dist/commands/local-args.js +8 -0
- package/dist/commands/local.js +429 -34
- package/dist/local-state.js +6 -3
- package/dist/onboarding-roots.js +183 -18
- package/dist/raw-evidence-gc.js +122 -0
- package/package.json +1 -1
package/dist/local-state.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function pairLocalCollector(options = {}) {
|
|
|
106
106
|
device_name: deviceName,
|
|
107
107
|
claimed_owner_email: claimedOwnerEmail,
|
|
108
108
|
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
109
|
-
});
|
|
109
|
+
}, options.pairingAccessToken);
|
|
110
110
|
options.onPairStarted?.(startResponse);
|
|
111
111
|
const sessionFile = await pollPairRequest(fetchImpl, dashboardUrl, {
|
|
112
112
|
paths,
|
|
@@ -459,10 +459,13 @@ function normalizeDashboardUrl(value) {
|
|
|
459
459
|
throw new Error("Dashboard URL cannot be empty.");
|
|
460
460
|
return normalized;
|
|
461
461
|
}
|
|
462
|
-
async function postPairStart(fetchImpl, dashboardUrl, body) {
|
|
462
|
+
async function postPairStart(fetchImpl, dashboardUrl, body, accessToken) {
|
|
463
463
|
const response = await fetchImpl(`${dashboardUrl}/api/ambient/pair/start`, {
|
|
464
464
|
method: "POST",
|
|
465
|
-
headers: {
|
|
465
|
+
headers: {
|
|
466
|
+
"Content-Type": "application/json",
|
|
467
|
+
...(accessToken ? { "Authorization": `Bearer ${accessToken}` } : {}),
|
|
468
|
+
},
|
|
466
469
|
body: JSON.stringify(body),
|
|
467
470
|
});
|
|
468
471
|
const parsed = await readResponseJson(response);
|
package/dist/onboarding-roots.js
CHANGED
|
@@ -1,67 +1,101 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { discoverGitWorktrees } from "./repo-identity.js";
|
|
4
5
|
import { normalizeCollectionRoots } from "./root-normalization.js";
|
|
5
6
|
export const COLLECTION_ROOT_REQUIRED = "collection_root_required";
|
|
7
|
+
export const HOME_ROOT_CONSENT_PROMPT = "Collect your ENTIRE home folder? Any git repo you ever create under it will be collected automatically. Type 'everything' to confirm: ";
|
|
6
8
|
export async function resolveOnboardingRoots(options) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const explicitInput = options.explicitRoots ?? [];
|
|
10
|
+
const explicit = normalizeRootsDetailed(explicitInput, {
|
|
11
|
+
homeDir: options.homeDir,
|
|
12
|
+
allowHomeRoot: options.allowHomeRoot,
|
|
13
|
+
});
|
|
14
|
+
if (explicit.roots.length > 0) {
|
|
15
|
+
explainRejectedRoots(options, explicit.rejected);
|
|
16
|
+
return resolvedRoots(options, explicit.roots, "explicit", true);
|
|
10
17
|
}
|
|
11
|
-
|
|
18
|
+
if (hasRootInput(explicitInput) && explicit.rejected.length > 0) {
|
|
19
|
+
if (!options.interactive) {
|
|
20
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${rootRejectionExplanation(explicit.rejected[0], options)}`);
|
|
21
|
+
}
|
|
22
|
+
explainRejectedRoots(options, explicit.rejected);
|
|
23
|
+
const homeOffer = await counterOfferHomeDirectoryRepos(options, explicit.rejected);
|
|
24
|
+
if (homeOffer)
|
|
25
|
+
return homeOffer;
|
|
26
|
+
const homeOptIn = await promptForHomeRootOptIn(options, explicit.rejected);
|
|
27
|
+
if (homeOptIn)
|
|
28
|
+
return homeOptIn;
|
|
29
|
+
return promptForRoots(options, "Collection root(s), comma-separated: ");
|
|
30
|
+
}
|
|
31
|
+
const savedRoots = normalizeRootsDetailed([
|
|
12
32
|
...(options.savedRoots ?? []),
|
|
13
33
|
...(options.config?.default_repo_paths ?? []),
|
|
14
|
-
]
|
|
34
|
+
], {
|
|
35
|
+
homeDir: options.homeDir,
|
|
36
|
+
allowHomeRoot: options.allowHomeRoot,
|
|
37
|
+
}).roots;
|
|
15
38
|
if (savedRoots.length > 0) {
|
|
16
39
|
if (!options.interactive) {
|
|
17
|
-
return
|
|
40
|
+
return resolvedRoots(options, savedRoots, "saved", false);
|
|
18
41
|
}
|
|
19
42
|
const confirmed = await requirePrompt(options).confirm(savedRootsPrompt(savedRoots));
|
|
20
43
|
if (confirmed) {
|
|
21
|
-
return
|
|
44
|
+
return resolvedRoots(options, savedRoots, "saved", true);
|
|
22
45
|
}
|
|
23
46
|
return promptForRoots(options, "Collection root(s), comma-separated: ");
|
|
24
47
|
}
|
|
25
48
|
if (!options.interactive) {
|
|
26
|
-
throw new Error(`${COLLECTION_ROOT_REQUIRED}:
|
|
49
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${missingCollectionRootMessage(options)}`);
|
|
27
50
|
}
|
|
28
51
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
29
52
|
const cwdRoot = likelyBliRootFromCwd(cwd);
|
|
30
53
|
if (cwdRoot) {
|
|
31
54
|
const confirmed = await requirePrompt(options).confirm(`Collect from ${cwdRoot}? [Y/n] `);
|
|
32
55
|
if (confirmed) {
|
|
33
|
-
return
|
|
56
|
+
return resolvedRoots(options, [cwdRoot], "cwd_likely_root", true);
|
|
34
57
|
}
|
|
35
58
|
}
|
|
36
59
|
const homeRoot = path.join(options.homeDir ?? os.homedir(), "BLI");
|
|
37
60
|
if (await directoryExists(homeRoot)) {
|
|
38
61
|
const confirmed = await requirePrompt(options).confirm(`I found ${homeRoot}. Collect from there? [Y/n] `);
|
|
39
62
|
if (confirmed) {
|
|
40
|
-
return
|
|
63
|
+
return resolvedRoots(options, [path.resolve(homeRoot)], "home_likely_root", true);
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
66
|
return promptForRoots(options, "Collection root(s), comma-separated: ");
|
|
44
67
|
}
|
|
45
68
|
export function normalizeRoots(roots) {
|
|
69
|
+
return normalizeRootsDetailed(roots).roots;
|
|
70
|
+
}
|
|
71
|
+
export function normalizeRootsDetailed(roots, options = {}) {
|
|
46
72
|
const seen = new Set();
|
|
47
73
|
const candidates = [];
|
|
74
|
+
const rejected = [];
|
|
75
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
48
76
|
for (const root of roots) {
|
|
49
77
|
const trimmed = root.trim();
|
|
50
78
|
if (!trimmed)
|
|
51
79
|
continue;
|
|
52
80
|
for (const candidate of splitRootInput(trimmed)) {
|
|
53
81
|
const resolved = path.resolve(candidate);
|
|
54
|
-
if (resolved === path.parse(resolved).root)
|
|
55
|
-
|
|
56
|
-
if (resolved === os.homedir())
|
|
82
|
+
if (resolved === path.parse(resolved).root) {
|
|
83
|
+
rejected.push({ input: candidate, reason: "fs_root" });
|
|
57
84
|
continue;
|
|
85
|
+
}
|
|
86
|
+
if (resolved === homeDir) {
|
|
87
|
+
if (!options.allowHomeRoot) {
|
|
88
|
+
rejected.push({ input: candidate, reason: "home_dir" });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
58
92
|
if (seen.has(resolved))
|
|
59
93
|
continue;
|
|
60
94
|
seen.add(resolved);
|
|
61
95
|
candidates.push(resolved);
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
|
-
return normalizeCollectionRoots(candidates);
|
|
98
|
+
return { roots: normalizeCollectionRoots(candidates), rejected };
|
|
65
99
|
}
|
|
66
100
|
function splitRootInput(value) {
|
|
67
101
|
return value
|
|
@@ -71,11 +105,29 @@ function splitRootInput(value) {
|
|
|
71
105
|
}
|
|
72
106
|
async function promptForRoots(options, message) {
|
|
73
107
|
const prompt = requirePrompt(options);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
109
|
+
const detailed = normalizeRootsDetailed([await prompt.input(message)], {
|
|
110
|
+
homeDir: options.homeDir,
|
|
111
|
+
allowHomeRoot: options.allowHomeRoot,
|
|
112
|
+
});
|
|
113
|
+
if (detailed.roots.length > 0) {
|
|
114
|
+
explainRejectedRoots(options, detailed.rejected);
|
|
115
|
+
return resolvedRoots(options, detailed.roots, "prompt", true);
|
|
116
|
+
}
|
|
117
|
+
if (detailed.rejected.length > 0) {
|
|
118
|
+
explainRejectedRoots(options, detailed.rejected);
|
|
119
|
+
const homeOffer = await counterOfferHomeDirectoryRepos(options, detailed.rejected);
|
|
120
|
+
if (homeOffer)
|
|
121
|
+
return homeOffer;
|
|
122
|
+
const homeOptIn = await promptForHomeRootOptIn(options, detailed.rejected);
|
|
123
|
+
if (homeOptIn)
|
|
124
|
+
return homeOptIn;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
prompt.message?.(missingCollectionRootMessage(options));
|
|
128
|
+
}
|
|
77
129
|
}
|
|
78
|
-
|
|
130
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${missingCollectionRootMessage(options)}`);
|
|
79
131
|
}
|
|
80
132
|
function savedRootsPrompt(roots) {
|
|
81
133
|
if (roots.length === 1) {
|
|
@@ -93,6 +145,119 @@ function requirePrompt(options) {
|
|
|
93
145
|
}
|
|
94
146
|
return options.prompt;
|
|
95
147
|
}
|
|
148
|
+
function hasRootInput(inputs) {
|
|
149
|
+
return inputs.some((input) => input.trim().length > 0);
|
|
150
|
+
}
|
|
151
|
+
function explainRejectedRoots(options, rejections) {
|
|
152
|
+
if (!options.interactive || rejections.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const prompt = requirePrompt(options);
|
|
155
|
+
for (const rejection of rejections) {
|
|
156
|
+
prompt.message?.(rootRejectionPromptHint(rejection, options));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function rootRejectionExplanation(rejection, options = {}) {
|
|
160
|
+
switch (rejection.reason) {
|
|
161
|
+
case "home_dir":
|
|
162
|
+
return homeRootTutorial(options.homeDir);
|
|
163
|
+
case "fs_root":
|
|
164
|
+
return filesystemRootTutorial(options.homeDir);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function rootRejectionPromptHint(rejection, options = {}) {
|
|
168
|
+
switch (rejection.reason) {
|
|
169
|
+
case "home_dir":
|
|
170
|
+
return homeRootTutorial(options.homeDir);
|
|
171
|
+
case "fs_root":
|
|
172
|
+
return filesystemRootTutorial(options.homeDir);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function counterOfferHomeDirectoryRepos(options, rejections) {
|
|
176
|
+
const homeRejection = rejections.find((rejection) => rejection.reason === "home_dir");
|
|
177
|
+
if (!homeRejection)
|
|
178
|
+
return null;
|
|
179
|
+
const prompt = requirePrompt(options);
|
|
180
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
181
|
+
const discover = options.discoverGitWorktrees ?? discoverGitWorktrees;
|
|
182
|
+
const worktrees = await discover(homeDir, {
|
|
183
|
+
maxDepth: 3,
|
|
184
|
+
maxWorktrees: 50,
|
|
185
|
+
}).catch(() => []);
|
|
186
|
+
const roots = normalizeCollectionRoots([...new Set(worktrees.map((worktree) => path.resolve(worktree.repo_root)))]);
|
|
187
|
+
if (roots.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
const confirmed = await prompt.confirm([
|
|
190
|
+
"Your home folder can't be a collection root by default:",
|
|
191
|
+
` I found ${roots.length} git repos under ${homeDir} that are safer to collect.`,
|
|
192
|
+
"Discovered repos:",
|
|
193
|
+
...roots.map((root) => `- ${root}`),
|
|
194
|
+
"What you can do:",
|
|
195
|
+
" 1) Use these repos: answer y",
|
|
196
|
+
" 2) Pick different folders: answer n, then paste specific paths",
|
|
197
|
+
" 3) Collect everything anyway: answer n, then type everything at the next prompt",
|
|
198
|
+
"Collect from these? [Y/n] ",
|
|
199
|
+
].join("\n"));
|
|
200
|
+
return confirmed ? resolvedRoots(options, roots, "prompt", true) : null;
|
|
201
|
+
}
|
|
202
|
+
async function promptForHomeRootOptIn(options, rejections) {
|
|
203
|
+
if (!rejections.some((rejection) => rejection.reason === "home_dir"))
|
|
204
|
+
return null;
|
|
205
|
+
const answer = await requirePrompt(options).input(HOME_ROOT_CONSENT_PROMPT);
|
|
206
|
+
if (answer.trim().toLowerCase() !== "everything")
|
|
207
|
+
return null;
|
|
208
|
+
return {
|
|
209
|
+
roots: [path.resolve(options.homeDir ?? os.homedir())],
|
|
210
|
+
source: "prompt",
|
|
211
|
+
confirmed: true,
|
|
212
|
+
homeRootOptIn: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function resolvedRoots(options, roots, source, confirmed) {
|
|
216
|
+
const result = { roots, source, confirmed };
|
|
217
|
+
if (options.allowHomeRoot && roots.some((root) => isHomeRoot(root, options.homeDir))) {
|
|
218
|
+
result.homeRootOptIn = true;
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function isHomeRoot(root, homeDir) {
|
|
223
|
+
return path.resolve(root) === path.resolve(homeDir ?? os.homedir());
|
|
224
|
+
}
|
|
225
|
+
function homeRootTutorial(homeDirInput) {
|
|
226
|
+
const homeDir = path.resolve(homeDirInput ?? os.homedir());
|
|
227
|
+
const bliRoot = path.join(homeDir, "BLI");
|
|
228
|
+
const otherRoot = path.join(homeDir, "other");
|
|
229
|
+
return [
|
|
230
|
+
"Your home folder can't be a collection root by default:",
|
|
231
|
+
` scanning all of ${homeDir} would include personal folders and every repo you ever create.`,
|
|
232
|
+
"What you can do:",
|
|
233
|
+
` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
|
|
234
|
+
" 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
|
|
235
|
+
" 3) Collect everything anyway (company machine): rerun with --allow-home-root",
|
|
236
|
+
].join("\n");
|
|
237
|
+
}
|
|
238
|
+
function filesystemRootTutorial(homeDirInput) {
|
|
239
|
+
const homeDir = path.resolve(homeDirInput ?? os.homedir());
|
|
240
|
+
const bliRoot = path.join(homeDir, "BLI");
|
|
241
|
+
const otherRoot = path.join(homeDir, "other");
|
|
242
|
+
return [
|
|
243
|
+
"Your filesystem root can't be a collection root:",
|
|
244
|
+
` scanning all of ${path.parse(homeDir).root} would include system folders, private data, and every mounted repo.`,
|
|
245
|
+
"What you can do:",
|
|
246
|
+
` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
|
|
247
|
+
" 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
250
|
+
export function missingCollectionRootMessage(options = {}) {
|
|
251
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
252
|
+
return [
|
|
253
|
+
"No collection root was confirmed:",
|
|
254
|
+
" Cockpit only scans folders you name or confirm.",
|
|
255
|
+
"What you can do:",
|
|
256
|
+
` 1) Paste one work parent: ${path.join(homeDir, "BLI")}`,
|
|
257
|
+
` 2) Paste specific folders: ${path.join(homeDir, "repo-a")},${path.join(homeDir, "repo-b")}`,
|
|
258
|
+
" 3) Stop and rerun with a flag: cockpit onboard --workspace ~/BLI",
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
96
261
|
function likelyBliRootFromCwd(cwd) {
|
|
97
262
|
const parts = cwd.split(path.sep).filter(Boolean);
|
|
98
263
|
const index = parts.lastIndexOf("BLI");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readRawEvidenceCursor } from "./cursors/raw-evidence-cursor.js";
|
|
5
|
+
const RAW_EVIDENCE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
const SYNC_LOG_MAX_BYTES = 50 * 1024 * 1024;
|
|
7
|
+
// GC hashes every file in every old-but-kept dir to confirm uploads. Running
|
|
8
|
+
// that on each 15-min sync would re-read the same gigabytes ~96x/day on
|
|
9
|
+
// machines with unconfirmed evidence, so GC is throttled to once per day.
|
|
10
|
+
const GC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
export async function runRawEvidenceLocalGc(paths, env = process.env, now = new Date()) {
|
|
12
|
+
if (env["COCKPIT_DISABLE_GC"] === "1") {
|
|
13
|
+
return {
|
|
14
|
+
skipped: true,
|
|
15
|
+
removed_dirs: 0,
|
|
16
|
+
freed_bytes: 0,
|
|
17
|
+
rotated_sync_log: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const throttleMarker = path.join(paths.state_dir, ".last-raw-evidence-gc");
|
|
21
|
+
const lastRun = await fs.stat(throttleMarker).catch(() => null);
|
|
22
|
+
if (lastRun && now.getTime() - lastRun.mtimeMs < GC_MIN_INTERVAL_MS) {
|
|
23
|
+
return {
|
|
24
|
+
skipped: true,
|
|
25
|
+
removed_dirs: 0,
|
|
26
|
+
freed_bytes: 0,
|
|
27
|
+
rotated_sync_log: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
await fs.mkdir(paths.state_dir, { recursive: true }).catch(() => undefined);
|
|
31
|
+
await fs.writeFile(throttleMarker, now.toISOString()).catch(() => undefined);
|
|
32
|
+
const cursor = await readRawEvidenceCursor(paths);
|
|
33
|
+
const uploadedHashes = new Set(Object.keys(cursor.objects));
|
|
34
|
+
const rawEvidenceRoot = path.join(paths.state_dir, "raw-evidence");
|
|
35
|
+
const entries = await fs.readdir(rawEvidenceRoot, { withFileTypes: true }).catch(() => []);
|
|
36
|
+
let removedDirs = 0;
|
|
37
|
+
let freedBytes = 0;
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isDirectory() || !entry.name.startsWith("work-"))
|
|
40
|
+
continue;
|
|
41
|
+
const dir = path.join(rawEvidenceRoot, entry.name);
|
|
42
|
+
const dirStats = await fs.stat(dir).catch(() => null);
|
|
43
|
+
if (!dirStats?.isDirectory())
|
|
44
|
+
continue;
|
|
45
|
+
if (now.getTime() - dirStats.mtimeMs < RAW_EVIDENCE_RETENTION_MS)
|
|
46
|
+
continue;
|
|
47
|
+
const inspection = await inspectRawEvidenceDirForGc(dir, uploadedHashes);
|
|
48
|
+
if (!inspection.deletable)
|
|
49
|
+
continue;
|
|
50
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
51
|
+
if (await exists(dir))
|
|
52
|
+
continue;
|
|
53
|
+
removedDirs += 1;
|
|
54
|
+
freedBytes += inspection.byteSize;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
skipped: false,
|
|
58
|
+
removed_dirs: removedDirs,
|
|
59
|
+
freed_bytes: freedBytes,
|
|
60
|
+
rotated_sync_log: await rotateSyncLog(paths),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function rawEvidenceGcSummary(result) {
|
|
64
|
+
return `raw-evidence GC: removed ${result.removed_dirs} dirs, freed ~${formatMb(result.freed_bytes)} MB`;
|
|
65
|
+
}
|
|
66
|
+
async function inspectRawEvidenceDirForGc(dir, uploadedHashes) {
|
|
67
|
+
const files = await listFiles(dir).catch(() => null);
|
|
68
|
+
if (!files)
|
|
69
|
+
return { deletable: false, byteSize: 0 };
|
|
70
|
+
if (files.length === 0)
|
|
71
|
+
return { deletable: true, byteSize: 0 };
|
|
72
|
+
let byteSize = 0;
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const bytes = await fs.readFile(file).catch(() => null);
|
|
75
|
+
if (!bytes)
|
|
76
|
+
return { deletable: false, byteSize: 0 };
|
|
77
|
+
const digest = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
78
|
+
if (!uploadedHashes.has(digest)) {
|
|
79
|
+
return { deletable: false, byteSize: 0 };
|
|
80
|
+
}
|
|
81
|
+
byteSize += bytes.byteLength;
|
|
82
|
+
}
|
|
83
|
+
return { deletable: true, byteSize };
|
|
84
|
+
}
|
|
85
|
+
async function listFiles(root) {
|
|
86
|
+
const files = [];
|
|
87
|
+
const stack = [root];
|
|
88
|
+
while (stack.length > 0) {
|
|
89
|
+
const current = stack.pop();
|
|
90
|
+
if (!current)
|
|
91
|
+
continue;
|
|
92
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const full = path.join(current, entry.name);
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
stack.push(full);
|
|
97
|
+
}
|
|
98
|
+
else if (entry.isFile()) {
|
|
99
|
+
files.push(full);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
throw new Error("raw-evidence directory contains unsupported entry");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
async function rotateSyncLog(paths) {
|
|
109
|
+
const syncLog = path.join(paths.state_dir, "sync.log");
|
|
110
|
+
const info = await fs.stat(syncLog).catch(() => null);
|
|
111
|
+
if (!info?.isFile() || info.size <= SYNC_LOG_MAX_BYTES)
|
|
112
|
+
return false;
|
|
113
|
+
await fs.truncate(syncLog, 0).catch(() => undefined);
|
|
114
|
+
const after = await fs.stat(syncLog).catch(() => null);
|
|
115
|
+
return after?.isFile() === true && after.size === 0;
|
|
116
|
+
}
|
|
117
|
+
async function exists(target) {
|
|
118
|
+
return fs.stat(target).then(() => true, () => false);
|
|
119
|
+
}
|
|
120
|
+
function formatMb(bytes) {
|
|
121
|
+
return (bytes / (1024 * 1024)).toFixed(2);
|
|
122
|
+
}
|