agent-afk 5.249.2 → 5.249.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.
@@ -11,9 +11,46 @@
11
11
  */
12
12
 
13
13
  import { execSync, execFileSync } from 'node:child_process';
14
- import { existsSync, readFileSync } from 'node:fs';
14
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
15
15
  import { homedir } from 'node:os';
16
16
  import { join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ // ─── Module-scope derived paths ────────────────────────────────────────────
20
+ // Computed once so restartLaunchdServices and the main block share the same
21
+ // decoded, symlink-resolved package root rather than each re-deriving it.
22
+ // fileURLToPath decodes percent-encoded characters (e.g. spaces → %20) that
23
+ // new URL().pathname leaves encoded and that path.join then mis-handles.
24
+ const _pkgRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..');
25
+
26
+ // ─── Exported helpers ───────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Pure function. Returns true when this module is the Node.js entry-point
30
+ * (i.e. run directly, not imported). Accepts the two pieces of identity as
31
+ * parameters so it can be unit-tested with arbitrary values.
32
+ *
33
+ * Comparison uses fileURLToPath (not a raw `file://` string) so that
34
+ * percent-encoded characters in the install path — e.g. `/my%20dir/` — do not
35
+ * cause a false negative. realpathSync resolves symlinks in argv[1] so that
36
+ * globally-installed bins (typically symlinked into /usr/local/bin) compare
37
+ * equal to the real file recorded in import.meta.url.
38
+ *
39
+ * @param {string} metaUrl - import.meta.url of the calling module.
40
+ * @param {string} [argv1] - process.argv[1]; may be absent or non-existent.
41
+ * @returns {boolean}
42
+ */
43
+ export function isMainModule(metaUrl, argv1) {
44
+ if (!argv1) return false;
45
+ try {
46
+ return fileURLToPath(metaUrl) === realpathSync(argv1);
47
+ } catch {
48
+ // argv[1] does not exist on disk (e.g. a piped script, a test runner
49
+ // that synthesises a path). Fall back to a plain string comparison so
50
+ // the guard never throws at module evaluation time.
51
+ return fileURLToPath(metaUrl) === argv1;
52
+ }
53
+ }
17
54
 
18
55
  /**
19
56
  * Pure function. Determines whether the npm bin directory is on PATH.
@@ -49,11 +86,11 @@ export function detectPathGap(prefix, pathEnv) {
49
86
  * This is a belt-and-suspenders guard: even if npm_config_global were
50
87
  * wrong or missing, a .git marker unambiguously rules out a global install.
51
88
  *
52
- * // History: Daemon was unconditionally restarted on every postinstall.
53
- * // A cron job that ran `pnpm install` inside a repo worktree triggered this
54
- * // path and restarted the running daemon mid-session, orphaning 438 subagents
55
- * // and leaving the daemon stopped. The guard introduced here ensures restarts
56
- * // occur ONLY for genuine global upgrades. See PR #2194.
89
+ * History: Daemon was unconditionally restarted on every postinstall.
90
+ * A cron job that ran `pnpm install` inside a repo worktree triggered this
91
+ * path and restarted the running daemon mid-session, orphaning 438 subagents
92
+ * and leaving the daemon stopped. The guard introduced here ensures restarts
93
+ * occur ONLY for genuine global upgrades. See PR #2195.
57
94
  *
58
95
  * @param {string} pkgRoot - Absolute path to the package root.
59
96
  * @param {object} [env] - Environment variable bag; defaults to process.env.
@@ -143,12 +180,12 @@ export function isManualBotRunning(pidFilePath, probeFn = process.kill) {
143
180
  * is swallowed so the install never fails. macOS only — the caller gates on
144
181
  * platform; elsewhere ~/Library/LaunchAgents won't exist so nothing restarts.
145
182
  *
146
- * // Invariant: the daemon is NOT stateless. It may be mid-run on one or more
147
- * // scheduled agent sessions. This function MUST only be called from a global
148
- * // package upgrade path (verified by isGlobalInstall()) — never from a local
149
- * // `pnpm install` inside a source checkout or worktree. The caller in the main
150
- * // block enforces this via isGlobalInstall(); callers in tests must pass stubs
151
- * // for execFn/restartFn so real launchctl is never invoked.
183
+ * Invariant: the daemon is NOT stateless. It may be mid-run on one or more
184
+ * scheduled agent sessions. This function MUST only be called from a global
185
+ * package upgrade path (verified by isGlobalInstall()) — never from a local
186
+ * `pnpm install` inside a source checkout or worktree. The caller in the main
187
+ * block enforces this via isGlobalInstall(); callers in tests must pass stubs
188
+ * for execFn/restartFn so real launchctl is never invoked.
152
189
  *
153
190
  * Label / path / domain conventions mirror src/service/launchd/paths.ts
154
191
  * (labelFor → `com.afk.<name>`, plist under ~/Library/LaunchAgents, guiDomain →
@@ -186,8 +223,9 @@ export function restartLaunchdServices(opts = {}) {
186
223
  //
187
224
  // __dirname equivalent for ESM: derive from import.meta.url.
188
225
  // scripts/postinstall.mjs → dist/cli.mjs (package root / dist/).
189
- const scriptDir = new URL('.', import.meta.url).pathname;
190
- const pkgRoot = join(scriptDir, '..');
226
+ // fileURLToPath is used (not .pathname) so percent-encoded characters in the
227
+ // path are decoded correctly — e.g. a space-containing install dir on macOS.
228
+ const pkgRoot = _pkgRoot;
191
229
  const cliMjs = join(pkgRoot, 'dist', 'cli.mjs');
192
230
 
193
231
  // Invariant: `afk service restart` is the correct single command — it
@@ -241,10 +279,14 @@ export function restartLaunchdServices(opts = {}) {
241
279
 
242
280
  // ─── Main block ─────────────────────────────────────────────────────────────
243
281
  // Guard: only run when executed directly (not when imported by tests).
282
+ // isMainModule() handles percent-encoded paths and symlinked global bins
283
+ // correctly; the raw `file://${process.argv[1]}` comparison it replaces
284
+ // would return false when the install path contains spaces or other chars
285
+ // that import.meta.url encodes as percent-sequences.
244
286
  const isMain =
245
287
  typeof process !== 'undefined' &&
246
288
  typeof process.argv !== 'undefined' &&
247
- import.meta.url === `file://${process.argv[1]}`;
289
+ isMainModule(import.meta.url, process.argv[1]);
248
290
 
249
291
  if (isMain) {
250
292
  // Only meaningful on macOS/Linux where PATH-based installs are common.
@@ -316,9 +358,7 @@ if (isMain) {
316
358
  // guard below enforces this; it fails closed toward NOT restarting whenever
317
359
  // either detection signal is absent or ambiguous.
318
360
  if (process.platform === 'darwin') {
319
- const scriptDir = new URL('.', import.meta.url).pathname;
320
- const pkgRoot = join(scriptDir, '..');
321
- if (isGlobalInstall(pkgRoot, process.env, existsSync)) {
361
+ if (isGlobalInstall(_pkgRoot, process.env, existsSync)) {
322
362
  try {
323
363
  const restarted = restartLaunchdServices({
324
364
  labels: ['com.afk.daemon'],