@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coworker-jp/aidr",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "AIDR setup CLI - installs ai-scanner hooks for 19+ AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -589,7 +589,7 @@ export async function run(argv) {
589
589
  program
590
590
  .name("aidr")
591
591
  .description("AIDR setup CLI — installs ai-scanner hooks for AI coding agents")
592
- .version("0.1.3");
592
+ .version("0.1.5");
593
593
 
594
594
  program
595
595
  .command("install")
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 fallback (system cron, runs as root)
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. systemd's `StandardOutput=append:`
41
- // (systemd 240+) and launchd's `StandardOutPath` redirect daemon stderr/stdout
42
- // here; for cron, the entry uses a literal `>> ... 2>&1` redirect.
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 systemd / launchd / cron
166
- // entry so the daemon's first invocation has a target to append to.
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
- const platform = process.platform;
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
- // /etc/cron.d entries run as the user named in the line. We embed the env
319
- // file source via a small inline script. stdout + stderr are appended to
320
- // the world-readable log so non-root users can read recent runs without
321
- // sudo (replaces the old `2>/dev/null` which deliberately swallowed errors).
322
- const entry = `0 */${interval} * * * root . ${SYSTEM_ENV_FILE} && ${binaryPath} scan endpoint-info >> ${SCHEDULED_LOG_FILE} 2>&1 # aidr-scheduled
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 (mid-systemctl) doesn't
368
- // strand a service file that still references a missing env file.
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); }