@coworker-jp/aidr 0.1.3 → 0.1.5
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/package.json +1 -1
- package/src/cli.mjs +1 -1
- package/src/scheduled.mjs +33 -88
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
package/src/scheduled.mjs
CHANGED
|
@@ -22,24 +22,19 @@ export const SYSTEM_USR_LOCAL_BIN = ROOT_PREFIX + "/usr/local/bin/ai-scanner";
|
|
|
22
22
|
const SYSTEM_ENV_DIR = ROOT_PREFIX + "/etc/aidr";
|
|
23
23
|
export const SYSTEM_ENV_FILE = path.join(SYSTEM_ENV_DIR, "aidr.env");
|
|
24
24
|
|
|
25
|
-
// Linux systemd
|
|
26
|
-
const SYSTEMD_DIR = ROOT_PREFIX + "/etc/systemd/system";
|
|
27
|
-
export const SYSTEMD_SERVICE = path.join(SYSTEMD_DIR, "aidr-scheduled.service");
|
|
28
|
-
export const SYSTEMD_TIMER = path.join(SYSTEMD_DIR, "aidr-scheduled.timer");
|
|
29
|
-
|
|
30
25
|
// macOS launchd (LaunchDaemons run as root, available pre-login)
|
|
31
26
|
export const LAUNCHD_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.aidr.scheduled.plist";
|
|
32
27
|
export const LAUNCHD_WRAPPER = path.join(SYSTEM_BIN_DIR, "aidr-scheduled-wrapper.sh");
|
|
33
28
|
|
|
34
|
-
// Linux cron
|
|
29
|
+
// Linux: cron.d entry, runs as root every N hours.
|
|
35
30
|
export const CRON_FILE = ROOT_PREFIX + "/etc/cron.d/aidr-scheduled";
|
|
36
31
|
|
|
37
32
|
// Shared world-readable system log directory. Both aidr-scheduled and
|
|
38
33
|
// coworker-sentinel daemons write here so non-root users can `tail` errors
|
|
39
34
|
// without sudo (memory: feedback_server_failures_never_burden_client.md).
|
|
40
|
-
// Mode 0755 root:root, files mode 0644.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
35
|
+
// Mode 0755 root:root, files mode 0644. launchd's `StandardOutPath` (macOS)
|
|
36
|
+
// redirects daemon stderr/stdout here; for Linux cron, the entry uses a
|
|
37
|
+
// literal `>> ... 2>&1` redirect.
|
|
43
38
|
export const SYSTEM_LOG_DIR = ROOT_PREFIX + "/var/log/coworker";
|
|
44
39
|
export const SCHEDULED_LOG_FILE = path.join(SYSTEM_LOG_DIR, "scheduled.log");
|
|
45
40
|
export const SENTINEL_LOG_FILE = path.join(SYSTEM_LOG_DIR, "sentinel.log");
|
|
@@ -134,11 +129,7 @@ export async function removeSystemLog(logFile, { dryRun = false } = {}) {
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
export async function isScheduledInstalled() {
|
|
137
|
-
return (
|
|
138
|
-
await exists(SYSTEMD_TIMER) ||
|
|
139
|
-
await exists(LAUNCHD_PLIST) ||
|
|
140
|
-
await exists(CRON_FILE)
|
|
141
|
-
);
|
|
132
|
+
return (await exists(LAUNCHD_PLIST)) || (await exists(CRON_FILE));
|
|
142
133
|
}
|
|
143
134
|
|
|
144
135
|
export async function installScheduled(opts) {
|
|
@@ -162,15 +153,12 @@ export async function installScheduled(opts) {
|
|
|
162
153
|
await writeEnvFile(accessKey, dryRun);
|
|
163
154
|
|
|
164
155
|
// Create the world-readable log dir + touch scheduled.log + drop the
|
|
165
|
-
// shared logrotate config. Both run before the
|
|
166
|
-
//
|
|
156
|
+
// shared logrotate config. Both run before the launchd / cron entry so
|
|
157
|
+
// the daemon's first invocation has a target to append to.
|
|
167
158
|
await ensureSystemLogDir(SCHEDULED_LOG_FILE, { dryRun });
|
|
168
159
|
await writeLogrotateConfig({ dryRun });
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
if (platform === "linux") {
|
|
172
|
-
await installSystemd({ dryRun, noActivate, interval, binaryPath });
|
|
173
|
-
} else if (platform === "darwin") {
|
|
161
|
+
if (process.platform === "darwin") {
|
|
174
162
|
await installLaunchd({ dryRun, noActivate, interval, binaryPath });
|
|
175
163
|
} else {
|
|
176
164
|
await installCronDir({ binaryPath, dryRun, interval });
|
|
@@ -197,57 +185,6 @@ async function writeEnvFile(accessKey, dryRun) {
|
|
|
197
185
|
await fs.writeFile(SYSTEM_ENV_FILE, envContent, { mode: 0o600 });
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
async function installSystemd({ binaryPath, dryRun, noActivate, interval = 4 }) {
|
|
201
|
-
// StandardOutput=append: requires systemd 240+ (released 2018). Modern
|
|
202
|
-
// distros (RHEL 9 / Ubuntu 20.04+ / Debian 11+) all support it. Older
|
|
203
|
-
// RHEL 8 (systemd 239) operators must redirect manually or upgrade.
|
|
204
|
-
const serviceContent = `[Unit]
|
|
205
|
-
Description=AIDR scheduled endpoint-info scan
|
|
206
|
-
After=network-online.target
|
|
207
|
-
Wants=network-online.target
|
|
208
|
-
|
|
209
|
-
[Service]
|
|
210
|
-
Type=oneshot
|
|
211
|
-
User=root
|
|
212
|
-
EnvironmentFile=${SYSTEM_ENV_FILE}
|
|
213
|
-
ExecStart=${binaryPath} scan endpoint-info
|
|
214
|
-
StandardOutput=append:${SCHEDULED_LOG_FILE}
|
|
215
|
-
StandardError=append:${SCHEDULED_LOG_FILE}
|
|
216
|
-
`;
|
|
217
|
-
|
|
218
|
-
const timerContent = `[Unit]
|
|
219
|
-
Description=AIDR scheduled endpoint-info scan timer
|
|
220
|
-
|
|
221
|
-
[Timer]
|
|
222
|
-
OnBootSec=5min
|
|
223
|
-
OnUnitActiveSec=${interval}h
|
|
224
|
-
Persistent=true
|
|
225
|
-
|
|
226
|
-
[Install]
|
|
227
|
-
WantedBy=timers.target
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
|
-
if (dryRun) {
|
|
231
|
-
console.log("[dry-run] Would create:", SYSTEMD_SERVICE, SYSTEMD_TIMER);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
await fs.mkdir(path.dirname(SYSTEMD_SERVICE), { recursive: true });
|
|
236
|
-
await fs.writeFile(SYSTEMD_SERVICE, serviceContent, { mode: 0o644 });
|
|
237
|
-
await fs.writeFile(SYSTEMD_TIMER, timerContent, { mode: 0o644 });
|
|
238
|
-
|
|
239
|
-
if (!noActivate) {
|
|
240
|
-
try {
|
|
241
|
-
await execFileP("systemctl", ["daemon-reload"]);
|
|
242
|
-
await execFileP("systemctl", ["enable", "--now", "aidr-scheduled.timer"]);
|
|
243
|
-
console.log(`Scheduled scan enabled via system systemd timer (every ${interval}h)`);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
console.warn("Warning: systemctl failed:", e.message);
|
|
246
|
-
console.warn("Timer files were written. Enable manually: sudo systemctl enable --now aidr-scheduled.timer");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
188
|
export async function installLaunchd({ binaryPath, dryRun, noActivate, interval = 4 }) {
|
|
252
189
|
// launchd does not support EnvironmentFile, so we use a tiny wrapper that
|
|
253
190
|
// sources /etc/aidr/aidr.env and execs the binary. Plist itself stays 0644
|
|
@@ -315,11 +252,13 @@ exec ${binaryPath} scan endpoint-info
|
|
|
315
252
|
}
|
|
316
253
|
|
|
317
254
|
async function installCronDir({ binaryPath, dryRun, interval = 4 }) {
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
255
|
+
// The aidr.env file is plain `KEY=VALUE` lines (no `export` prefix —
|
|
256
|
+
// systemd's EnvironmentFile= used by the sentinel service requires that
|
|
257
|
+
// form). Plain `. file` sets shell-local variables that don't propagate
|
|
258
|
+
// to the spawned ai-scanner process; `set -a` flips on auto-export so
|
|
259
|
+
// every assignment becomes an environment variable. Same pattern as the
|
|
260
|
+
// launchd wrapper on macOS.
|
|
261
|
+
const entry = `0 */${interval} * * * root set -a; . ${SYSTEM_ENV_FILE}; set +a; ${binaryPath} scan endpoint-info >> ${SCHEDULED_LOG_FILE} 2>&1
|
|
323
262
|
`;
|
|
324
263
|
if (dryRun) {
|
|
325
264
|
console.log("[dry-run] Would create:", CRON_FILE);
|
|
@@ -328,6 +267,22 @@ async function installCronDir({ binaryPath, dryRun, interval = 4 }) {
|
|
|
328
267
|
await fs.mkdir(path.dirname(CRON_FILE), { recursive: true });
|
|
329
268
|
await fs.writeFile(CRON_FILE, entry, { mode: 0o644 });
|
|
330
269
|
console.log(`Scheduled scan added to ${CRON_FILE} (every ${interval}h)`);
|
|
270
|
+
|
|
271
|
+
// Warn (don't fail) when no cron daemon is detected. The file is valid;
|
|
272
|
+
// the user just needs to install one (`apt install cron`, `dnf install
|
|
273
|
+
// cronie`, etc.) before the next scheduled hour for it to fire.
|
|
274
|
+
if (!await hasCronDaemon()) {
|
|
275
|
+
console.warn(
|
|
276
|
+
"WARNING: cron daemon not detected. Install one (`apt install cron` / `dnf install cronie` / `apk add dcron`) so /etc/cron.d/aidr-scheduled actually fires."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function hasCronDaemon() {
|
|
282
|
+
for (const candidate of ["/usr/sbin/cron", "/usr/sbin/crond", "/sbin/cron", "/sbin/crond"]) {
|
|
283
|
+
if (await exists(candidate)) return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
331
286
|
}
|
|
332
287
|
|
|
333
288
|
export async function uninstallScheduled({ dryRun = false, noActivate = false } = {}) {
|
|
@@ -337,16 +292,6 @@ export async function uninstallScheduled({ dryRun = false, noActivate = false }
|
|
|
337
292
|
throw new Error("uninstallScheduled requires root privileges");
|
|
338
293
|
}
|
|
339
294
|
|
|
340
|
-
if (await exists(SYSTEMD_TIMER)) {
|
|
341
|
-
if (!dryRun && !noActivate) {
|
|
342
|
-
try { await execFileP("systemctl", ["disable", "--now", "aidr-scheduled.timer"]); } catch { /* ignore */ }
|
|
343
|
-
}
|
|
344
|
-
for (const f of [SYSTEMD_SERVICE, SYSTEMD_TIMER]) {
|
|
345
|
-
if (!dryRun) { try { await fs.unlink(f); } catch { /* ignore */ } }
|
|
346
|
-
else { console.log("[dry-run] Would remove:", f); }
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
295
|
if (await exists(LAUNCHD_PLIST)) {
|
|
351
296
|
if (!dryRun && !noActivate) {
|
|
352
297
|
try { await execFileP("launchctl", ["bootout", "system", LAUNCHD_PLIST]); } catch { /* ignore */ }
|
|
@@ -364,8 +309,8 @@ export async function uninstallScheduled({ dryRun = false, noActivate = false }
|
|
|
364
309
|
else { console.log("[dry-run] Would remove:", CRON_FILE); }
|
|
365
310
|
}
|
|
366
311
|
|
|
367
|
-
// Remove the env file last so an aborted uninstall
|
|
368
|
-
//
|
|
312
|
+
// Remove the env file last so an aborted uninstall doesn't strand a
|
|
313
|
+
// schedule entry that still references a missing env file.
|
|
369
314
|
if (await exists(SYSTEM_ENV_FILE)) {
|
|
370
315
|
if (!dryRun) { try { await fs.unlink(SYSTEM_ENV_FILE); } catch { /* ignore */ } }
|
|
371
316
|
else { console.log("[dry-run] Would remove:", SYSTEM_ENV_FILE); }
|