@coworker-jp/aidr 0.1.10 → 0.1.18
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 +66 -14
- package/package.json +1 -1
- package/src/agents/standalone.mjs +3 -3
- package/src/binary-fetcher.mjs +6 -58
- package/src/cli.mjs +198 -151
- package/src/env-gate.mjs +208 -0
- package/src/red-team.mjs +169 -0
- package/src/scheduled.mjs +83 -222
- package/src/sentinel.mjs +143 -266
- package/src/templates.mjs +7 -6
- package/src/verify.mjs +3 -4
- package/src/browser-extension.mjs +0 -83
- package/src/invoker-resolve.mjs +0 -63
package/src/scheduled.mjs
CHANGED
|
@@ -2,13 +2,20 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { execFile } from "child_process";
|
|
4
4
|
import { promisify } from "util";
|
|
5
|
-
import { resolveInvokerUser } from "./invoker-resolve.mjs";
|
|
6
5
|
|
|
7
6
|
const execFileP = promisify(execFile);
|
|
8
7
|
|
|
9
|
-
// System-wide install paths (root-only)
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// System-wide install paths (root-only), shared with the legacy
|
|
9
|
+
// coworker-sentinel cleanup (sentinel.mjs). Historically this module wrote a
|
|
10
|
+
// cron.d / launchd schedule entry (`aidr install --scheduled`) that ran
|
|
11
|
+
// `ai-scanner scan endpoint-info` periodically; that mechanism is retired —
|
|
12
|
+
// endpoint-info collection now runs from the coworker-sentinel daemon's
|
|
13
|
+
// internal timer (installed via the OS-native installers, not aidr).
|
|
14
|
+
// What remains here is:
|
|
15
|
+
// - the path constants (still the canonical legacy system layout)
|
|
16
|
+
// - the shared log / logrotate cleanup helpers used by sentinel.mjs
|
|
17
|
+
// - cleanupLegacyScheduled(), which removes leftover schedule entries
|
|
18
|
+
// written by older aidr versions.
|
|
12
19
|
//
|
|
13
20
|
// `AIDR_SCHEDULED_ROOT` env (smoke / unit test only) prefixes every path so
|
|
14
21
|
// they land under a tmpdir like `/tmp/test123/opt/coworker/aidr/bin/...`
|
|
@@ -33,19 +40,21 @@ const SYSTEM_ENV_DIR = IS_WIN
|
|
|
33
40
|
: ROOT_PREFIX + "/etc/aidr";
|
|
34
41
|
export const SYSTEM_ENV_FILE = path.join(SYSTEM_ENV_DIR, "aidr.env");
|
|
35
42
|
|
|
36
|
-
// macOS launchd (LaunchDaemons
|
|
43
|
+
// Legacy macOS launchd entry (LaunchDaemons ran as root, pre-login) —
|
|
44
|
+
// written by `aidr install --scheduled` before the sentinel-daemon scheduler.
|
|
45
|
+
// Kept only so cleanupLegacyScheduled can find and remove old installs.
|
|
37
46
|
export const LAUNCHD_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.aidr.scheduled.plist";
|
|
47
|
+
export const LAUNCHD_LABEL = "jp.coworker.aidr.scheduled";
|
|
38
48
|
export const LAUNCHD_WRAPPER = path.join(SYSTEM_BIN_DIR, "aidr-scheduled-wrapper.sh");
|
|
39
49
|
|
|
40
|
-
// Linux
|
|
50
|
+
// Legacy Linux cron.d entry (same retirement).
|
|
41
51
|
export const CRON_FILE = ROOT_PREFIX + "/etc/cron.d/aidr-scheduled";
|
|
42
52
|
|
|
43
|
-
// Shared world-readable system log directory.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// literal `>> ... 2>&1` redirect.
|
|
53
|
+
// Shared world-readable system log directory. The coworker-sentinel daemon
|
|
54
|
+
// writes here so non-root users can `tail` errors without sudo (memory:
|
|
55
|
+
// feedback_server_failures_never_burden_client.md). Mode 0755 root:root,
|
|
56
|
+
// files mode 0644. SCHEDULED_LOG_FILE is the legacy aidr-scheduled log,
|
|
57
|
+
// kept only so cleanupLegacyScheduled can remove it.
|
|
49
58
|
export const SYSTEM_LOG_DIR = IS_WIN
|
|
50
59
|
? path.join(WIN_PROGRAMDATA, "coworker", "logs")
|
|
51
60
|
: ROOT_PREFIX + "/var/log/coworker";
|
|
@@ -63,26 +72,9 @@ async function exists(p) {
|
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* has a writable target even before any line is logged.
|
|
70
|
-
*/
|
|
71
|
-
export async function ensureSystemLogDir(logFile, { dryRun = false } = {}) {
|
|
72
|
-
if (dryRun) {
|
|
73
|
-
console.log("[dry-run] Would create:", SYSTEM_LOG_DIR, "(mode 0755) and touch", logFile, "(mode 0644)");
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
await fs.mkdir(SYSTEM_LOG_DIR, { recursive: true, mode: 0o755 });
|
|
77
|
-
if (!(await exists(logFile))) {
|
|
78
|
-
const fd = await fs.open(logFile, "w", 0o644);
|
|
79
|
-
await fd.close();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Drop /etc/logrotate.d/coworker covering both daemons. Idempotent: same
|
|
85
|
-
* content overwritten on every install. The logrotate command itself may
|
|
75
|
+
* Drop /etc/logrotate.d/coworker for the sentinel daemon log. Idempotent:
|
|
76
|
+
* used by cleanupLegacyScheduled to strip the retired scheduled.log stanza
|
|
77
|
+
* from configs written by older aidr versions. The logrotate command itself may
|
|
86
78
|
* not be present on minimal containers; the config file is harmless when
|
|
87
79
|
* no logrotate is installed (it simply isn't read). Operators on systems
|
|
88
80
|
* without logrotate should configure their own rotation.
|
|
@@ -91,17 +83,7 @@ export async function writeLogrotateConfig({ dryRun = false } = {}) {
|
|
|
91
83
|
// logrotate is a Unix tool; Windows has no equivalent here (the service can
|
|
92
84
|
// size-rotate its own audit log). No-op on Windows.
|
|
93
85
|
if (process.platform === "win32") return;
|
|
94
|
-
const body = `# Generated by aidr install. Covers
|
|
95
|
-
# coworker-sentinel (daily, more chatty under load).
|
|
96
|
-
/var/log/coworker/scheduled.log {
|
|
97
|
-
weekly
|
|
98
|
-
rotate 4
|
|
99
|
-
compress
|
|
100
|
-
missingok
|
|
101
|
-
notifempty
|
|
102
|
-
create 0644 root root
|
|
103
|
-
}
|
|
104
|
-
|
|
86
|
+
const body = `# Generated by aidr install. Covers the coworker-sentinel daemon log.
|
|
105
87
|
/var/log/coworker/sentinel.log {
|
|
106
88
|
daily
|
|
107
89
|
rotate 7
|
|
@@ -122,8 +104,8 @@ export async function writeLogrotateConfig({ dryRun = false } = {}) {
|
|
|
122
104
|
/**
|
|
123
105
|
* Remove the given daemon's log file. If both daemon log files are gone
|
|
124
106
|
* afterwards, also rmdir SYSTEM_LOG_DIR and remove the logrotate config.
|
|
125
|
-
* Used by
|
|
126
|
-
* matter (whichever
|
|
107
|
+
* Used by cleanupLegacyScheduled and uninstallSentinel; the order doesn't
|
|
108
|
+
* matter (whichever cleanup runs last wins the dir/logrotate removal).
|
|
127
109
|
*/
|
|
128
110
|
export async function removeSystemLog(logFile, { dryRun = false } = {}) {
|
|
129
111
|
if (await exists(logFile)) {
|
|
@@ -144,196 +126,75 @@ export async function removeSystemLog(logFile, { dryRun = false } = {}) {
|
|
|
144
126
|
}
|
|
145
127
|
}
|
|
146
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Detect leftovers of the retired `aidr install --scheduled` mechanism
|
|
131
|
+
* (cron.d entry / launchd plist / launchd wrapper) from older aidr versions.
|
|
132
|
+
*/
|
|
147
133
|
export async function isScheduledInstalled() {
|
|
148
|
-
return (await exists(LAUNCHD_PLIST))
|
|
134
|
+
return (await exists(LAUNCHD_PLIST))
|
|
135
|
+
|| (await exists(CRON_FILE))
|
|
136
|
+
|| (await exists(LAUNCHD_WRAPPER));
|
|
149
137
|
}
|
|
150
138
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Write the shared env file first; both Linux and macOS paths reference it.
|
|
169
|
-
await writeEnvFile(accessKey, dryRun);
|
|
170
|
-
|
|
171
|
-
// Create the world-readable log dir + touch scheduled.log + drop the
|
|
172
|
-
// shared logrotate config. Both run before the launchd / cron entry so
|
|
173
|
-
// the daemon's first invocation has a target to append to.
|
|
174
|
-
await ensureSystemLogDir(SCHEDULED_LOG_FILE, { dryRun });
|
|
175
|
-
await writeLogrotateConfig({ dryRun });
|
|
176
|
-
|
|
177
|
-
if (process.platform === "darwin") {
|
|
178
|
-
await installLaunchd({ dryRun, noActivate, interval, binaryPath });
|
|
179
|
-
} else {
|
|
180
|
-
await installCronDir({ binaryPath, dryRun, interval });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function writeEnvFile(accessKey, dryRun) {
|
|
185
|
-
// Capture the invoker username (SUDO_USER first, then `who am i` /
|
|
186
|
-
// `logname` / `loginctl` / $LOGNAME / $USER fallbacks) — the Rust
|
|
187
|
-
// binary resolves the full identity (Apple ID / git email / username)
|
|
188
|
-
// at scan time using `sudo -u <invoker>` so it reads the invoker's
|
|
189
|
-
// plist / gitconfig instead of root's empty ones. Pre-resolving here
|
|
190
|
-
// is the wrong layer because the Apple ID needs to be re-read in the
|
|
191
|
-
// right user context at runtime anyway (root has no MobileMeAccounts).
|
|
192
|
-
const invoker = resolveInvokerUser();
|
|
193
|
-
let envContent = `AI_SCANNER_ACCESS_KEY=${accessKey}\n`;
|
|
194
|
-
if (invoker) envContent += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
|
|
195
|
-
if (dryRun) {
|
|
196
|
-
console.log("[dry-run] Would create:", SYSTEM_ENV_FILE, "(mode 0600)");
|
|
197
|
-
if (invoker) console.log("[dry-run] invoker captured:", invoker);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
await fs.mkdir(SYSTEM_ENV_DIR, { recursive: true, mode: 0o755 });
|
|
201
|
-
await fs.writeFile(SYSTEM_ENV_FILE, envContent, { mode: 0o600 });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function installLaunchd({ binaryPath, dryRun, noActivate, interval = 4 }) {
|
|
205
|
-
// launchd does not support EnvironmentFile, so we use a tiny wrapper that
|
|
206
|
-
// sources /etc/aidr/aidr.env and execs the binary. Plist itself stays 0644
|
|
207
|
-
// (launchctl bootstrap requires it readable); the secret lives in the env
|
|
208
|
-
// file at 0600.
|
|
209
|
-
const wrapperContent = `#!/bin/bash
|
|
210
|
-
# Generated by aidr install --scheduled. Sources the system env file and
|
|
211
|
-
# execs ai-scanner so the access key is not embedded in the LaunchDaemon plist.
|
|
212
|
-
set -a
|
|
213
|
-
. ${SYSTEM_ENV_FILE}
|
|
214
|
-
set +a
|
|
215
|
-
# launchd starts daemons with a narrow default PATH (/usr/bin:/bin:/usr/sbin:/sbin)
|
|
216
|
-
# that omits Homebrew. Without this, scan endpoint-info silently misses brew
|
|
217
|
-
# packages because Command::new("brew") spawn fails — gem still works because
|
|
218
|
-
# /usr/bin/gem is on the default PATH. Both Apple Silicon (/opt/homebrew/bin)
|
|
219
|
-
# and Intel (/usr/local/bin) prefixes are safe to prepend on either arch since
|
|
220
|
-
# missing dirs in PATH are simply ignored at lookup time.
|
|
221
|
-
export PATH="/opt/homebrew/bin:/usr/local/bin:/opt/homebrew/sbin:/usr/local/sbin:\${PATH:-/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
222
|
-
exec ${binaryPath} scan endpoint-info
|
|
223
|
-
`;
|
|
224
|
-
|
|
225
|
-
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
226
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
227
|
-
<plist version="1.0">
|
|
228
|
-
<dict>
|
|
229
|
-
<key>Label</key>
|
|
230
|
-
<string>jp.coworker.aidr.scheduled</string>
|
|
231
|
-
<key>UserName</key>
|
|
232
|
-
<string>root</string>
|
|
233
|
-
<key>ProgramArguments</key>
|
|
234
|
-
<array>
|
|
235
|
-
<string>${LAUNCHD_WRAPPER}</string>
|
|
236
|
-
</array>
|
|
237
|
-
<key>StartInterval</key>
|
|
238
|
-
<integer>${interval * 3600}</integer>
|
|
239
|
-
<key>RunAtLoad</key>
|
|
240
|
-
<false/>
|
|
241
|
-
<key>StandardOutPath</key>
|
|
242
|
-
<string>${SCHEDULED_LOG_FILE}</string>
|
|
243
|
-
<key>StandardErrorPath</key>
|
|
244
|
-
<string>${SCHEDULED_LOG_FILE}</string>
|
|
245
|
-
</dict>
|
|
246
|
-
</plist>
|
|
247
|
-
`;
|
|
248
|
-
|
|
249
|
-
if (dryRun) {
|
|
250
|
-
console.log("[dry-run] Would create:", LAUNCHD_WRAPPER, LAUNCHD_PLIST);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
await fs.mkdir(path.dirname(LAUNCHD_WRAPPER), { recursive: true });
|
|
255
|
-
await fs.mkdir(path.dirname(LAUNCHD_PLIST), { recursive: true });
|
|
256
|
-
await fs.writeFile(LAUNCHD_WRAPPER, wrapperContent, { mode: 0o755 });
|
|
257
|
-
await fs.writeFile(LAUNCHD_PLIST, plistContent, { mode: 0o644 });
|
|
258
|
-
|
|
259
|
-
if (!noActivate) {
|
|
260
|
-
try {
|
|
261
|
-
await execFileP("launchctl", ["bootstrap", "system", LAUNCHD_PLIST]);
|
|
262
|
-
console.log(`Scheduled scan enabled via system LaunchDaemon (every ${interval}h)`);
|
|
263
|
-
} catch (e) {
|
|
264
|
-
console.warn("Warning: launchctl bootstrap failed:", e.message);
|
|
265
|
-
console.warn(`Plist written. Enable manually: sudo launchctl bootstrap system ${LAUNCHD_PLIST}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function installCronDir({ binaryPath, dryRun, interval = 4 }) {
|
|
271
|
-
// The aidr.env file is plain `KEY=VALUE` lines (no `export` prefix —
|
|
272
|
-
// systemd's EnvironmentFile= used by the sentinel service requires that
|
|
273
|
-
// form). Plain `. file` sets shell-local variables that don't propagate
|
|
274
|
-
// to the spawned ai-scanner process; `set -a` flips on auto-export so
|
|
275
|
-
// every assignment becomes an environment variable. Same pattern as the
|
|
276
|
-
// launchd wrapper on macOS.
|
|
277
|
-
const entry = `0 */${interval} * * * root set -a; . ${SYSTEM_ENV_FILE}; set +a; ${binaryPath} scan endpoint-info >> ${SCHEDULED_LOG_FILE} 2>&1
|
|
278
|
-
`;
|
|
279
|
-
if (dryRun) {
|
|
280
|
-
console.log("[dry-run] Would create:", CRON_FILE);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
await fs.mkdir(path.dirname(CRON_FILE), { recursive: true });
|
|
284
|
-
await fs.writeFile(CRON_FILE, entry, { mode: 0o644 });
|
|
285
|
-
console.log(`Scheduled scan added to ${CRON_FILE} (every ${interval}h)`);
|
|
286
|
-
|
|
287
|
-
// Warn (don't fail) when no cron daemon is detected. The file is valid;
|
|
288
|
-
// the user just needs to install one (`apt install cron`, `dnf install
|
|
289
|
-
// cronie`, etc.) before the next scheduled hour for it to fire.
|
|
290
|
-
if (!await hasCronDaemon()) {
|
|
291
|
-
console.warn(
|
|
292
|
-
"WARNING: cron daemon not detected. Install one (`apt install cron` / `dnf install cronie` / `apk add dcron`) so /etc/cron.d/aidr-scheduled actually fires."
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
async function hasCronDaemon() {
|
|
298
|
-
for (const candidate of ["/usr/sbin/cron", "/usr/sbin/crond", "/sbin/cron", "/sbin/crond"]) {
|
|
299
|
-
if (await exists(candidate)) return true;
|
|
300
|
-
}
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export async function uninstallScheduled({ dryRun = false, noActivate = false } = {}) {
|
|
305
|
-
noActivate = noActivate || process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1";
|
|
306
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Remove every artifact the retired `aidr install --scheduled` flow wrote:
|
|
141
|
+
* - /etc/cron.d/aidr-scheduled (Linux)
|
|
142
|
+
* - /Library/LaunchDaemons/jp.coworker.aidr.scheduled.plist (macOS,
|
|
143
|
+
* with a best-effort `launchctl bootout system/<label>` first)
|
|
144
|
+
* - the launchd wrapper script in SYSTEM_BIN_DIR
|
|
145
|
+
* - /var/log/coworker/scheduled.log (+ the log dir / logrotate config if
|
|
146
|
+
* no daemon log remains — see removeSystemLog)
|
|
147
|
+
* - the stale scheduled.log stanza in /etc/logrotate.d/coworker when the
|
|
148
|
+
* config survives because sentinel is still installed
|
|
149
|
+
*
|
|
150
|
+
* MUST NOT touch /etc/aidr/aidr.env: the coworker-sentinel daemon reads its
|
|
151
|
+
* access key from that shared env file.
|
|
152
|
+
*
|
|
153
|
+
* Idempotent; safe to call when nothing is installed.
|
|
154
|
+
*/
|
|
155
|
+
export async function cleanupLegacyScheduled({ dryRun = false } = {}) {
|
|
307
156
|
if (!isRoot()) {
|
|
308
|
-
throw new Error("
|
|
157
|
+
throw new Error("cleanupLegacyScheduled requires root privileges (system paths under /etc and /opt)");
|
|
309
158
|
}
|
|
159
|
+
const noActivate = process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1";
|
|
310
160
|
|
|
311
161
|
if (await exists(LAUNCHD_PLIST)) {
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
if (!dryRun) {
|
|
316
|
-
try { await fs.unlink(LAUNCHD_PLIST); } catch { /* ignore */ }
|
|
317
|
-
try { await fs.unlink(LAUNCHD_WRAPPER); } catch { /* ignore */ }
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
console.log("[dry-run] Would remove:", LAUNCHD_PLIST);
|
|
318
164
|
} else {
|
|
319
|
-
|
|
165
|
+
if (!noActivate) {
|
|
166
|
+
// Best-effort: unloads the running LaunchDaemon so the entry stops
|
|
167
|
+
// firing immediately. Fails harmlessly when it was never bootstrapped
|
|
168
|
+
// (or off-macOS in tests).
|
|
169
|
+
try { await execFileP("launchctl", ["bootout", `system/${LAUNCHD_LABEL}`]); } catch { /* ignore */ }
|
|
170
|
+
}
|
|
171
|
+
try { await fs.unlink(LAUNCHD_PLIST); } catch { /* ignore */ }
|
|
320
172
|
}
|
|
321
173
|
}
|
|
322
174
|
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
175
|
+
for (const f of [LAUNCHD_WRAPPER, CRON_FILE]) {
|
|
176
|
+
if (await exists(f)) {
|
|
177
|
+
if (dryRun) { console.log("[dry-run] Would remove:", f); }
|
|
178
|
+
else { try { await fs.unlink(f); } catch { /* ignore */ } }
|
|
179
|
+
}
|
|
326
180
|
}
|
|
327
181
|
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
if (!dryRun) { try { await fs.unlink(SYSTEM_ENV_FILE); } catch { /* ignore */ } }
|
|
332
|
-
else { console.log("[dry-run] Would remove:", SYSTEM_ENV_FILE); }
|
|
333
|
-
}
|
|
182
|
+
// NOTE: SYSTEM_ENV_FILE (/etc/aidr/aidr.env) is deliberately NOT removed —
|
|
183
|
+
// the coworker-sentinel daemon reads AI_SCANNER_ACCESS_KEY /
|
|
184
|
+
// AI_SCANNER_INVOKER_USER from it.
|
|
334
185
|
|
|
335
|
-
// Drop the scheduled log file. removeSystemLog also rmdirs
|
|
336
|
-
// and deletes /etc/logrotate.d/coworker
|
|
337
|
-
// (i.e. sentinel is
|
|
186
|
+
// Drop the legacy scheduled log file. removeSystemLog also rmdirs
|
|
187
|
+
// SYSTEM_LOG_DIR and deletes /etc/logrotate.d/coworker when no daemon log
|
|
188
|
+
// remains (i.e. sentinel is not installed either).
|
|
338
189
|
await removeSystemLog(SCHEDULED_LOG_FILE, { dryRun });
|
|
190
|
+
|
|
191
|
+
// When the logrotate config survives (sentinel still installed), older
|
|
192
|
+
// installs left a stale scheduled.log stanza in it — rewrite with the
|
|
193
|
+
// current sentinel-only content.
|
|
194
|
+
if (!dryRun && (await exists(LOGROTATE_FILE))) {
|
|
195
|
+
const body = await fs.readFile(LOGROTATE_FILE, "utf8").catch(() => "");
|
|
196
|
+
if (body.includes("scheduled.log")) {
|
|
197
|
+
await writeLogrotateConfig({ dryRun });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
339
200
|
}
|