@coworker-jp/aidr 0.1.2 → 0.1.3

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.2",
3
+ "version": "0.1.3",
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.2");
592
+ .version("0.1.3");
593
593
 
594
594
  program
595
595
  .command("install")
@@ -0,0 +1,63 @@
1
+ // Best-effort invoker username resolution at install time.
2
+ //
3
+ // `aidr install --scheduled` / `--with-sentinel` writes the unix username
4
+ // into `/etc/aidr/aidr.env` as `AI_SCANNER_INVOKER_USER`. The Rust daemon
5
+ // reads it back to resolve git email / Apple ID / $HOME of the actual
6
+ // installer (root daemons have no useful identity of their own).
7
+ //
8
+ // In the auto-sudo path the CLI re-execs itself via `sudo -E`, which sets
9
+ // SUDO_USER. But several real-world contexts arrive here as bare root
10
+ // without SUDO_USER:
11
+ // - `ssh root@host` then `npx ... install --scheduled` (no sudo prefix)
12
+ // - container / CI where the user is already root
13
+ // - sudo policy that strips SUDO_USER
14
+ //
15
+ // We walk a fallback chain so the resulting daemon snapshot reports the
16
+ // real user instead of `actor_id=unknown`. Each step is best-effort and
17
+ // never throws — we return `null` when no signal is available, which is
18
+ // the honest outcome for genuinely identity-less contexts (CI runners).
19
+
20
+ import { execSync } from "node:child_process";
21
+
22
+ function tryExec(cmd) {
23
+ try {
24
+ return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
25
+ } catch {
26
+ return "";
27
+ }
28
+ }
29
+
30
+ function isUsable(name) {
31
+ return name && name !== "root";
32
+ }
33
+
34
+ export function resolveInvokerUser() {
35
+ if (process.env.SUDO_USER) return process.env.SUDO_USER;
36
+
37
+ // `who am i` reads utmp, so it returns the actual login user even after
38
+ // `sudo -i` / `su -`. Empty under non-tty / no utmp.
39
+ const whoUser = tryExec("who am i").split(/\s+/)[0];
40
+ if (isUsable(whoUser)) return whoUser;
41
+
42
+ // POSIX `logname` gives controlling-tty owner. Slightly different code
43
+ // path from `who am i` — kept as a separate fallback because they fail
44
+ // independently on minimal images.
45
+ const logname = tryExec("logname");
46
+ if (isUsable(logname)) return logname;
47
+
48
+ // systemd Linux: `loginctl list-sessions` columns are: SESSION UID USER ...
49
+ const loginctl = tryExec("loginctl list-sessions --no-legend");
50
+ for (const line of loginctl.split("\n")) {
51
+ const fields = line.trim().split(/\s+/);
52
+ if (fields.length >= 3 && isUsable(fields[2])) return fields[2];
53
+ }
54
+
55
+ // Env fallbacks (root-filtered) so a user-shell install without sudo at
56
+ // all (rare for --scheduled, possible for plain --agent without root)
57
+ // still captures the obvious answer.
58
+ for (const env of ["LOGNAME", "USER"]) {
59
+ if (isUsable(process.env[env])) return process.env[env];
60
+ }
61
+
62
+ return null;
63
+ }
package/src/scheduled.mjs CHANGED
@@ -2,6 +2,7 @@ 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";
5
6
 
6
7
  const execFileP = promisify(execFile);
7
8
 
@@ -177,14 +178,14 @@ export async function installScheduled(opts) {
177
178
  }
178
179
 
179
180
  async function writeEnvFile(accessKey, dryRun) {
180
- // Capture only the SUDO_USER (the unix username of whoever ran
181
- // `sudo aidr install ...`) the Rust binary will resolve the full
182
- // identity (Apple ID / git email / username) at scan time using
183
- // `sudo -u <invoker>` so it reads the invoker's plist / gitconfig
184
- // instead of root's empty ones. Pre-resolving here is the wrong
185
- // layer because the Apple ID needs to be re-read in the right user
186
- // context at runtime anyway (root has no MobileMeAccounts.plist).
187
- const invoker = process.env.SUDO_USER || null;
181
+ // Capture the invoker username (SUDO_USER first, then `who am i` /
182
+ // `logname` / `loginctl` / $LOGNAME / $USER fallbacks) the Rust
183
+ // binary resolves the full identity (Apple ID / git email / username)
184
+ // at scan time using `sudo -u <invoker>` so it reads the invoker's
185
+ // plist / gitconfig instead of root's empty ones. Pre-resolving here
186
+ // is the wrong layer because the Apple ID needs to be re-read in the
187
+ // right user context at runtime anyway (root has no MobileMeAccounts).
188
+ const invoker = resolveInvokerUser();
188
189
  let envContent = `AI_SCANNER_ACCESS_KEY=${accessKey}\n`;
189
190
  if (invoker) envContent += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
190
191
  if (dryRun) {
package/src/sentinel.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
  import fs from "node:fs/promises";
14
14
  import path from "node:path";
15
15
  import { execFile, spawnSync } from "node:child_process";
16
+ import { resolveInvokerUser } from "./invoker-resolve.mjs";
16
17
  import { promisify } from "node:util";
17
18
  import { fetchSentinel, detectPlatform } from "./binary-fetcher.mjs";
18
19
  import {
@@ -155,7 +156,7 @@ export async function installSentinel(opts = {}) {
155
156
  appended += `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`;
156
157
  }
157
158
  if (!content.includes("AI_SCANNER_INVOKER_USER")) {
158
- const invoker = process.env.SUDO_USER;
159
+ const invoker = resolveInvokerUser();
159
160
  if (invoker) {
160
161
  appended += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
161
162
  }