@code-yeongyu/senpi 2026.9.6 → 2026.9.7-2

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.
Files changed (194) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/core/agent-session.d.ts +51 -5
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +168 -48
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts.map +1 -1
  7. package/dist/core/compaction/compaction.js +2 -1
  8. package/dist/core/compaction/compaction.js.map +1 -1
  9. package/dist/core/compaction/stuck-overflow.d.ts +11 -0
  10. package/dist/core/compaction/stuck-overflow.d.ts.map +1 -0
  11. package/dist/core/compaction/stuck-overflow.js +17 -0
  12. package/dist/core/compaction/stuck-overflow.js.map +1 -0
  13. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts.map +1 -1
  14. package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -21
  15. package/dist/core/extensions/builtin/cache-keepalive/index.js.map +1 -1
  16. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js +36 -19
  18. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js.map +1 -1
  19. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js +21 -10
  21. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js.map +1 -1
  22. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts +4 -17
  23. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js +23 -33
  25. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js.map +1 -1
  26. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js +1 -0
  28. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js.map +1 -1
  29. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js +5 -3
  31. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js.map +1 -1
  32. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.d.ts.map +1 -1
  33. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js +8 -3
  34. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js.map +1 -1
  35. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts +6 -5
  36. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts.map +1 -1
  37. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js +6 -5
  38. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js.map +1 -1
  39. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts +8 -0
  40. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/compaction/circuit-breaker.js +8 -0
  42. package/dist/core/extensions/builtin/compaction/circuit-breaker.js.map +1 -1
  43. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +24 -1
  45. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  46. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts +18 -0
  47. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts.map +1 -0
  48. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js +20 -0
  49. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js.map +1 -0
  50. package/dist/core/extensions/builtin/compaction/index.js +2 -2
  51. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  52. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +3 -0
  53. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts.map +1 -1
  54. package/dist/core/extensions/builtin/compaction/lane-policy.js +47 -23
  55. package/dist/core/extensions/builtin/compaction/lane-policy.js.map +1 -1
  56. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +6 -4
  57. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  58. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +19 -1
  59. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/goal/cache-warm.js +23 -10
  61. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  62. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
  63. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
  64. package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
  65. package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
  66. package/dist/core/extensions/builtin/goal/continuation.d.ts +3 -1
  67. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/goal/continuation.js +4 -0
  69. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  70. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +3 -0
  71. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
  72. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +14 -3
  73. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
  74. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +1 -1
  75. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  76. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -3
  77. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  78. package/dist/core/extensions/builtin/hooks/plugin-manifest.js +2 -2
  79. package/dist/core/extensions/builtin/hooks/plugin-manifest.js.map +1 -1
  80. package/dist/core/extensions/builtin/hooks/safety.js +2 -2
  81. package/dist/core/extensions/builtin/hooks/safety.js.map +1 -1
  82. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/permission-system/external-dir.js +5 -22
  84. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  85. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  86. package/dist/core/extensions/builtin/permission-system/parsers.js +5 -7
  87. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  88. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  89. package/dist/core/extensions/builtin/terminal/monitor-registry.js +14 -7
  90. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  91. package/dist/core/package-identity.d.ts +12 -0
  92. package/dist/core/package-identity.d.ts.map +1 -0
  93. package/dist/core/package-identity.js +57 -0
  94. package/dist/core/package-identity.js.map +1 -0
  95. package/dist/core/resource-loader.d.ts +0 -2
  96. package/dist/core/resource-loader.d.ts.map +1 -1
  97. package/dist/core/resource-loader.js +8 -60
  98. package/dist/core/resource-loader.js.map +1 -1
  99. package/dist/core/tools/bounded-realpath.d.ts +28 -0
  100. package/dist/core/tools/bounded-realpath.d.ts.map +1 -0
  101. package/dist/core/tools/bounded-realpath.js +53 -0
  102. package/dist/core/tools/bounded-realpath.js.map +1 -0
  103. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  104. package/dist/core/tools/file-mutation-queue.js +15 -13
  105. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  106. package/dist/core/tools/filesystem-policy.d.ts +7 -0
  107. package/dist/core/tools/filesystem-policy.d.ts.map +1 -1
  108. package/dist/core/tools/filesystem-policy.js +25 -15
  109. package/dist/core/tools/filesystem-policy.js.map +1 -1
  110. package/dist/core/trust-manager.d.ts.map +1 -1
  111. package/dist/core/trust-manager.js +13 -2
  112. package/dist/core/trust-manager.js.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +1 -0
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/modes/interactive/tips/catalog/memory-tips.d.ts.map +1 -1
  117. package/dist/modes/interactive/tips/catalog/memory-tips.js +6 -0
  118. package/dist/modes/interactive/tips/catalog/memory-tips.js.map +1 -1
  119. package/dist/modes/interactive/tips/catalog/subagent-tips.d.ts.map +1 -1
  120. package/dist/modes/interactive/tips/catalog/subagent-tips.js +6 -0
  121. package/dist/modes/interactive/tips/catalog/subagent-tips.js.map +1 -1
  122. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  123. package/dist/modes/rpc/host-lifecycle.js +31 -5
  124. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  125. package/dist/modes/rpc/host-watchdog.d.ts +11 -0
  126. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  127. package/dist/modes/rpc/host-watchdog.js +16 -2
  128. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  129. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  130. package/dist/modes/rpc/multi-session-host.js +54 -16
  131. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  132. package/dist/modes/rpc/socket-ownership.d.ts +46 -0
  133. package/dist/modes/rpc/socket-ownership.d.ts.map +1 -0
  134. package/dist/modes/rpc/socket-ownership.js +178 -0
  135. package/dist/modes/rpc/socket-ownership.js.map +1 -0
  136. package/dist/utils/fs-watch.d.ts +17 -0
  137. package/dist/utils/fs-watch.d.ts.map +1 -1
  138. package/dist/utils/fs-watch.js +38 -10
  139. package/dist/utils/fs-watch.js.map +1 -1
  140. package/dist/utils/paths.d.ts +33 -0
  141. package/dist/utils/paths.d.ts.map +1 -1
  142. package/dist/utils/paths.js +109 -3
  143. package/dist/utils/paths.js.map +1 -1
  144. package/docs/compaction-guide.md +2 -2
  145. package/docs/compaction.md +2 -2
  146. package/docs/development.md +3 -2
  147. package/docs/providers.md +1 -1
  148. package/docs/rpc.md +1 -1
  149. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +29 -0
  150. package/node_modules/@code-yeongyu/senpi-codemode/README.md +14 -1
  151. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +1 -0
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-factory.ts +3 -0
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +7 -0
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +7 -0
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/kernel.ts +6 -2
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +2 -2
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +3 -0
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +29 -0
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +10 -6
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.d.ts +16 -0
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.js +44 -8
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +3 -2
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +3 -0
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +11 -3
  166. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/kernel.ts +6 -2
  167. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/session-env.ts +56 -0
  168. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/runtime-asset.ts +40 -14
  169. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +3 -0
  170. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +9 -1
  171. package/node_modules/@code-yeongyu/senpi-codemode/src/output/output-meta.ts +26 -2
  172. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/image.ts +12 -3
  173. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/azure-openai-responses.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  186. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  188. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +14 -3
  189. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  190. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  191. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  192. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  193. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  194. package/package.json +7 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-ownership.js","sourceRoot":"","sources":["../../../src/modes/rpc/socket-ownership.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ownership-checked removal for Unix domain socket path entries.\n *\n * A socket path is a rendezvous name, not a capability: once a host unlinks\n * its entry, any other process may bind the same path. Teardown that removes\n * \"its\" socket by path alone can therefore delete a NEWER host's freshly\n * published entry after a takeover renamed a replacement over that path. The\n * startup path already refuses to touch a socket owned by a live server\n * (`prepareSocketPath`); these helpers give every teardown path the same rule:\n * capture the bound entry's filesystem identity (dev + ino) at listen time and\n * unlink only while the path still stat-matches that identity.\n */\nimport { mkdir, readFile, rename, stat, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/** Filesystem identity of one bound socket path entry. */\nexport interface SocketFileIdentity {\n\treadonly dev: number;\n\treadonly ino: number;\n}\n\n/**\n * Name of the sidecar, inside the lifecycle supervisor's private scratch\n * directory, that records which public socket entry that supervisor bound. The\n * directory is per-supervisor, so a replacement supervisor's token can never be\n * mistaken for the old one's.\n */\nexport const PUBLIC_SOCKET_IDENTITY_FILE = \"public-socket.owner\";\n\n/** Default bound on waiting for a supervisor to publish its ownership token. */\nexport const SOCKET_IDENTITY_WAIT_MS = 30_000;\n\nfunction sameSocketIdentity(a: SocketFileIdentity, b: SocketFileIdentity): boolean {\n\treturn a.dev === b.dev && a.ino === b.ino;\n}\n\n/** Reads the current identity of a socket path entry; `undefined` once absent. */\nexport async function statSocketIdentity(socketPath: string): Promise<SocketFileIdentity | undefined> {\n\ttry {\n\t\tconst { dev, ino } = await stat(socketPath);\n\t\treturn { dev, ino };\n\t} catch (cause) {\n\t\tif (isNodeErrorCode(cause, \"ENOENT\")) return undefined;\n\t\tthrow cause;\n\t}\n}\n\n/** Records a bound socket identity for teardown paths that cannot hold it in memory. */\nexport async function writeSocketIdentityFile(path: string, identity: SocketFileIdentity): Promise<void> {\n\tawait mkdir(dirname(path), { recursive: true, mode: 0o700 });\n\tconst tmpPath = `${path}.${process.pid}.tmp`;\n\tawait writeFile(tmpPath, `${JSON.stringify(identity)}\\n`, { mode: 0o600 });\n\tawait rename(tmpPath, path);\n}\n\nexport async function readSocketIdentityFile(path: string): Promise<SocketFileIdentity | undefined> {\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(path, \"utf8\");\n\t} catch (cause) {\n\t\tif (isNodeErrorCode(cause, \"ENOENT\")) return undefined;\n\t\tthrow cause;\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\t// Truncated or in-progress JSON is not a token: callers wait or leave the path.\n\t\treturn undefined;\n\t}\n\tif (\n\t\ttypeof parsed !== \"object\" ||\n\t\tparsed === null ||\n\t\ttypeof (parsed as { dev?: unknown }).dev !== \"number\" ||\n\t\ttypeof (parsed as { ino?: unknown }).ino !== \"number\"\n\t) {\n\t\treturn undefined;\n\t}\n\treturn { dev: (parsed as { dev: number }).dev, ino: (parsed as { ino: number }).ino };\n}\n\n/**\n * Waits, bounded, for an identity file to appear. The lifecycle supervisor\n * publishes its public-socket token after this host's internal listener is\n * ready, so supervised hosts poll briefly instead of assuming ordering.\n */\nexport async function waitForSocketIdentityFile(\n\tpath: string,\n\ttimeoutMs: number = SOCKET_IDENTITY_WAIT_MS,\n\tintervalMs = 25,\n): Promise<SocketFileIdentity | undefined> {\n\tconst deadline = Date.now() + timeoutMs;\n\tfor (;;) {\n\t\tconst identity = await readSocketIdentityFile(path);\n\t\tif (identity !== undefined) return identity;\n\t\tif (Date.now() >= deadline) return undefined;\n\t\tawait delayUnrefed(intervalMs);\n\t}\n}\n\n/**\n * Unlinks a socket path only while it still refers to the entry `identity`\n * describes. An absent path is nothing to do; a mismatched identity means a\n * newer host owns the name now and is left alone; an unknown identity can\n * never be proven ours, so the path is left and the decision is logged.\n */\nexport async function unlinkOwnedSocket(\n\tsocketPath: string,\n\tidentity: SocketFileIdentity | undefined,\n\tlog: (message: string) => void,\n): Promise<void> {\n\tif (process.platform === \"win32\" || socketPath.startsWith(\"\\0\")) return;\n\tlet current: SocketFileIdentity | undefined;\n\ttry {\n\t\tcurrent = await statSocketIdentity(socketPath);\n\t} catch (cause) {\n\t\tlog(`socket ${socketPath} ownership could not be verified (${errorMessage(cause)}); leaving it`);\n\t\treturn;\n\t}\n\tif (current === undefined) return;\n\tif (identity === undefined) {\n\t\tlog(`socket path ${socketPath} ownership unknown; leaving it`);\n\t\treturn;\n\t}\n\tif (!sameSocketIdentity(current, identity)) {\n\t\tlog(`socket path ${socketPath} now owned by another host; leaving it`);\n\t\treturn;\n\t}\n\ttry {\n\t\tawait unlink(socketPath);\n\t} catch (cause) {\n\t\tif (!isNodeErrorCode(cause, \"ENOENT\")) {\n\t\t\tlog(`socket ${socketPath} removal failed (${errorMessage(cause)})`);\n\t\t}\n\t}\n}\n\n/**\n * Runs `close` with the socket path's current entry shielded from libuv's\n * close-time unlink.\n *\n * libuv unlinks the NAME a pipe was bound to when its listening handle\n * closes - even when a takeover has renamed a DIFFERENT socket over that\n * name, so an ordinary `server.close()` silently deletes the newer host's\n * freshly published entry. Moving the current entry aside for the duration\n * of the close (libuv's unlink then hits ENOENT) and restoring it afterwards\n * keeps the filesystem exactly as it was; ownership-checked removal via\n * `unlinkOwnedSocket` decides afterwards what may actually be deleted.\n */\nexport async function shieldSocketDuringClose(socketPath: string, close: () => Promise<void>): Promise<void> {\n\tif (process.platform === \"win32\" || socketPath.startsWith(\"\\0\")) return close();\n\tconst shieldPath = `${socketPath}.shield-${process.pid}`;\n\tlet shielded = false;\n\ttry {\n\t\tif ((await statSocketIdentity(socketPath)) !== undefined) {\n\t\t\tawait rename(socketPath, shieldPath);\n\t\t\tshielded = true;\n\t\t}\n\t} catch {\n\t\t// Without a shield the close degrades to today's behavior; the\n\t\t// ownership-checked unlink still runs afterwards.\n\t}\n\ttry {\n\t\tawait close();\n\t} finally {\n\t\tif (shielded) {\n\t\t\ttry {\n\t\t\t\tawait rename(shieldPath, socketPath);\n\t\t\t} catch {\n\t\t\t\t// Best effort: a failed restore leaves the entry under the shield\n\t\t\t\t// name; startup probes treat a dead entry as replaceable.\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction delayUnrefed(ms: number): Promise<void> {\n\treturn new Promise((resolve) => {\n\t\tconst timer = setTimeout(resolve, ms);\n\t\ttimer.unref?.();\n\t});\n}\n\nfunction errorMessage(cause: unknown): string {\n\treturn cause instanceof Error ? cause.message : String(cause);\n}\n\nfunction isNodeErrorCode(cause: unknown, code: string): boolean {\n\treturn cause instanceof Error && \"code\" in cause && cause.code === code;\n}\n"]}
@@ -1,5 +1,22 @@
1
1
  import { type FSWatcher, type WatchListener, type WatchOptions } from "node:fs";
2
2
  export declare const FS_WATCH_RETRY_DELAY_MS = 5000;
3
3
  export declare function closeWatcher(watcher: FSWatcher | null | undefined): void;
4
+ /**
5
+ * The path to hand to `fs.watch`. On Windows libuv aborts the whole process when a directory
6
+ * watch is created through a non-canonical (8.3 short-name) path (issue #1229), so the canonical
7
+ * form is looked up there. Elsewhere the caller's path is used as-is: POSIX realpath opens every
8
+ * directory it resolves, which blocks forever on a wedged autofs trigger, and fs.watch does not
9
+ * need the canonical form on those platforms.
10
+ */
11
+ export declare function canonicalWatchPath(path: string): string;
12
+ /**
13
+ * Prove that a directory can be opened before a synchronous `fs.watch` touches it. The watcher
14
+ * opens the directory on the calling thread; on a wedged mount (a macOS autofs trigger whose
15
+ * automounter never answers) that open never returns and would block the host main thread. This
16
+ * probe performs the same open on the async pool, so a caller can bound it with a deadline. It
17
+ * rejects with the open error (for example EACCES on an execute-only directory, which fs.watch
18
+ * would refuse as well).
19
+ */
20
+ export declare function probeDirectoryOpenable(directory: string): Promise<void>;
4
21
  export declare function watchWithErrorHandler(path: string, listener: WatchListener<string>, onError: () => void, options?: WatchOptions): FSWatcher | null;
5
22
  //# sourceMappingURL=fs-watch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs-watch.d.ts","sourceRoot":"","sources":["../../src/utils/fs-watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAS,MAAM,SAAS,CAAC;AAErG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAUxE;AAED,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAC/B,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,CAAC,EAAE,YAAY,GACpB,SAAS,GAAG,IAAI,CAiBlB"}
1
+ {"version":3,"file":"fs-watch.d.ts","sourceRoot":"","sources":["../../src/utils/fs-watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAS,MAAM,SAAS,CAAC;AAGrG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAUxE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7E;AAED,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAC/B,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,CAAC,EAAE,YAAY,GACpB,SAAS,GAAG,IAAI,CASlB"}
@@ -1,4 +1,5 @@
1
1
  import { realpathSync, watch } from "node:fs";
2
+ import { opendir } from "node:fs/promises";
2
3
  export const FS_WATCH_RETRY_DELAY_MS = 5000;
3
4
  export function closeWatcher(watcher) {
4
5
  if (!watcher) {
@@ -11,18 +12,45 @@ export function closeWatcher(watcher) {
11
12
  // Ignore watcher close errors
12
13
  }
13
14
  }
15
+ /**
16
+ * The path to hand to `fs.watch`. On Windows libuv aborts the whole process when a directory
17
+ * watch is created through a non-canonical (8.3 short-name) path (issue #1229), so the canonical
18
+ * form is looked up there. Elsewhere the caller's path is used as-is: POSIX realpath opens every
19
+ * directory it resolves, which blocks forever on a wedged autofs trigger, and fs.watch does not
20
+ * need the canonical form on those platforms.
21
+ */
22
+ export function canonicalWatchPath(path) {
23
+ if (process.platform !== "win32")
24
+ return path;
25
+ try {
26
+ return realpathSync.native(path);
27
+ }
28
+ catch {
29
+ // Keep the raw path when it does not exist yet.
30
+ return path;
31
+ }
32
+ }
33
+ /**
34
+ * Prove that a directory can be opened before a synchronous `fs.watch` touches it. The watcher
35
+ * opens the directory on the calling thread; on a wedged mount (a macOS autofs trigger whose
36
+ * automounter never answers) that open never returns and would block the host main thread. This
37
+ * probe performs the same open on the async pool, so a caller can bound it with a deadline. It
38
+ * rejects with the open error (for example EACCES on an execute-only directory, which fs.watch
39
+ * would refuse as well).
40
+ */
41
+ export async function probeDirectoryOpenable(directory) {
42
+ const handle = await opendir(directory);
43
+ try {
44
+ // Bun opens the directory lazily on the first read; Node opens it in opendir itself.
45
+ await handle.read();
46
+ }
47
+ finally {
48
+ await handle.close();
49
+ }
50
+ }
14
51
  export function watchWithErrorHandler(path, listener, onError, options) {
15
52
  try {
16
- let watchPath = path;
17
- if (process.platform === "win32") {
18
- try {
19
- watchPath = realpathSync.native(path);
20
- }
21
- catch {
22
- // Keep the raw path when it does not exist yet.
23
- }
24
- }
25
- const watcher = watch(watchPath, { ...options, encoding: "utf8" }, listener);
53
+ const watcher = watch(canonicalWatchPath(path), { ...options, encoding: "utf8" }, listener);
26
54
  watcher.on("error", onError);
27
55
  return watcher;
28
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fs-watch.js","sourceRoot":"","sources":["../../src/utils/fs-watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,YAAY,EAAyC,KAAK,EAAE,MAAM,SAAS,CAAC;AAErG,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,MAAM,UAAU,YAAY,CAAC,OAAqC;IACjE,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO;IACR,CAAC;IAED,IAAI,CAAC;QACJ,OAAO,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACR,8BAA8B;IAC/B,CAAC;AACF,CAAC;AAED,MAAM,UAAU,qBAAqB,CACpC,IAAY,EACZ,QAA+B,EAC/B,OAAmB,EACnB,OAAsB;IAEtB,IAAI,CAAC;QACJ,IAAI,SAAS,GAAG,IAAI,CAAC;QACrB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC;gBACJ,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACR,gDAAgD;YACjD,CAAC;QACF,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC7E,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7B,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC","sourcesContent":["import { type FSWatcher, realpathSync, type WatchListener, type WatchOptions, watch } from \"node:fs\";\n\nexport const FS_WATCH_RETRY_DELAY_MS = 5000;\n\nexport function closeWatcher(watcher: FSWatcher | null | undefined): void {\n\tif (!watcher) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\twatcher.close();\n\t} catch {\n\t\t// Ignore watcher close errors\n\t}\n}\n\nexport function watchWithErrorHandler(\n\tpath: string,\n\tlistener: WatchListener<string>,\n\tonError: () => void,\n\toptions?: WatchOptions,\n): FSWatcher | null {\n\ttry {\n\t\tlet watchPath = path;\n\t\tif (process.platform === \"win32\") {\n\t\t\ttry {\n\t\t\t\twatchPath = realpathSync.native(path);\n\t\t\t} catch {\n\t\t\t\t// Keep the raw path when it does not exist yet.\n\t\t\t}\n\t\t}\n\t\tconst watcher = watch(watchPath, { ...options, encoding: \"utf8\" }, listener);\n\t\twatcher.on(\"error\", onError);\n\t\treturn watcher;\n\t} catch {\n\t\tonError();\n\t\treturn null;\n\t}\n}\n"]}
1
+ {"version":3,"file":"fs-watch.js","sourceRoot":"","sources":["../../src/utils/fs-watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,YAAY,EAAyC,KAAK,EAAE,MAAM,SAAS,CAAC;AACrG,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,MAAM,UAAU,YAAY,CAAC,OAAqC;IACjE,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO;IACR,CAAC;IAED,IAAI,CAAC;QACJ,OAAO,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACR,8BAA8B;IAC/B,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC9C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAC9C,IAAI,CAAC;QACJ,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACR,gDAAgD;QAChD,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,SAAiB;IAC7D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,CAAC;QACJ,qFAAqF;QACrF,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;YAAS,CAAC;QACV,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,qBAAqB,CACpC,IAAY,EACZ,QAA+B,EAC/B,OAAmB,EACnB,OAAsB;IAEtB,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC5F,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7B,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC","sourcesContent":["import { type FSWatcher, realpathSync, type WatchListener, type WatchOptions, watch } from \"node:fs\";\nimport { opendir } from \"node:fs/promises\";\n\nexport const FS_WATCH_RETRY_DELAY_MS = 5000;\n\nexport function closeWatcher(watcher: FSWatcher | null | undefined): void {\n\tif (!watcher) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\twatcher.close();\n\t} catch {\n\t\t// Ignore watcher close errors\n\t}\n}\n\n/**\n * The path to hand to `fs.watch`. On Windows libuv aborts the whole process when a directory\n * watch is created through a non-canonical (8.3 short-name) path (issue #1229), so the canonical\n * form is looked up there. Elsewhere the caller's path is used as-is: POSIX realpath opens every\n * directory it resolves, which blocks forever on a wedged autofs trigger, and fs.watch does not\n * need the canonical form on those platforms.\n */\nexport function canonicalWatchPath(path: string): string {\n\tif (process.platform !== \"win32\") return path;\n\ttry {\n\t\treturn realpathSync.native(path);\n\t} catch {\n\t\t// Keep the raw path when it does not exist yet.\n\t\treturn path;\n\t}\n}\n\n/**\n * Prove that a directory can be opened before a synchronous `fs.watch` touches it. The watcher\n * opens the directory on the calling thread; on a wedged mount (a macOS autofs trigger whose\n * automounter never answers) that open never returns and would block the host main thread. This\n * probe performs the same open on the async pool, so a caller can bound it with a deadline. It\n * rejects with the open error (for example EACCES on an execute-only directory, which fs.watch\n * would refuse as well).\n */\nexport async function probeDirectoryOpenable(directory: string): Promise<void> {\n\tconst handle = await opendir(directory);\n\ttry {\n\t\t// Bun opens the directory lazily on the first read; Node opens it in opendir itself.\n\t\tawait handle.read();\n\t} finally {\n\t\tawait handle.close();\n\t}\n}\n\nexport function watchWithErrorHandler(\n\tpath: string,\n\tlistener: WatchListener<string>,\n\tonError: () => void,\n\toptions?: WatchOptions,\n): FSWatcher | null {\n\ttry {\n\t\tconst watcher = watch(canonicalWatchPath(path), { ...options, encoding: \"utf8\" }, listener);\n\t\twatcher.on(\"error\", onError);\n\t\treturn watcher;\n\t} catch {\n\t\tonError();\n\t\treturn null;\n\t}\n}\n"]}
@@ -17,6 +17,39 @@ export interface PathInputOptions {
17
17
  * entries.
18
18
  */
19
19
  export declare function canonicalizePath(path: string): string;
20
+ /**
21
+ * {@link canonicalizePath} that refuses to guess.
22
+ *
23
+ * The convenience form hands back its input when resolution fails, which is what most callers
24
+ * want: they compare two canonical spellings and an unresolvable path simply compares unequal.
25
+ * A security decision cannot use that, because the raw input is indistinguishable from a
26
+ * canonical answer - so a path that could not be resolved would be keyed, compared, or trusted
27
+ * under a spelling the filesystem never confirmed. This form throws instead, leaving the caller
28
+ * to decide what an unresolvable path means.
29
+ */
30
+ export declare function canonicalizePathStrict(path: string): string;
31
+ /**
32
+ * Resolve symlinks without ever open(2)-ing a component, tolerating every error.
33
+ *
34
+ * Bun's `fs.realpath*` opens every directory it resolves, so an autofs trigger such as macOS
35
+ * `/home` blocks the calling thread (on the host main thread that freezes the TUI) and an
36
+ * execute-only directory fails with EACCES; lstat needs only search permission and never mounts
37
+ * anything. Components from the first unresolvable one onward are kept verbatim, so a file that
38
+ * does not exist yet still lands where its symlinked parent points.
39
+ *
40
+ * Never throws, so the resolved path may be incomplete. Use it where an approximate answer is
41
+ * better than a blocked thread — the external-path classifier and the monitor parent identity —
42
+ * and use {@link realpathWithoutOpenStrict} where an incomplete answer must not be trusted.
43
+ */
44
+ export declare function realpathWithoutOpen(inputPath: string): string;
45
+ /**
46
+ * {@link realpathWithoutOpen} that refuses to answer with a guess: a missing component is still
47
+ * tolerated (its name is appended verbatim, which is what a not-yet-created target needs), but
48
+ * EACCES, EIO, ELOOP and hop exhaustion throw instead of returning an unresolved path. Callers
49
+ * that feed a canonical path into a containment or identity decision need this contract, because
50
+ * an unresolved path can name a different file than the one the I/O will reach.
51
+ */
52
+ export declare function realpathWithoutOpenStrict(inputPath: string): string;
20
53
  export declare function getFileRevision(path: string): string | undefined;
21
54
  /**
22
55
  * Returns true if the value is NOT a package source (npm:, git:, etc.)
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOhE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAclD;AAED,gGAAgG;AAChG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAyBnF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAsB,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAIlH;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAe7D"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAoED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CASnE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOhE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAclD;AAED,gGAAgG;AAChG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAyBnF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAsB,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAIlH;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAe7D"}
@@ -1,6 +1,6 @@
1
- import { realpathSync, statSync } from "node:fs";
1
+ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { isAbsolute, join, resolve as nodeResolvePath, relative, sep } from "node:path";
3
+ import { isAbsolute, join, resolve as nodeResolvePath, parse, relative, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawnProcessSync } from "./child-process.js";
6
6
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
@@ -12,12 +12,118 @@ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
12
12
  */
13
13
  export function canonicalizePath(path) {
14
14
  try {
15
- return realpathSync(path);
15
+ return realpathSync.native(path);
16
16
  }
17
17
  catch {
18
18
  return path;
19
19
  }
20
20
  }
21
+ /**
22
+ * {@link canonicalizePath} that refuses to guess.
23
+ *
24
+ * The convenience form hands back its input when resolution fails, which is what most callers
25
+ * want: they compare two canonical spellings and an unresolvable path simply compares unequal.
26
+ * A security decision cannot use that, because the raw input is indistinguishable from a
27
+ * canonical answer - so a path that could not be resolved would be keyed, compared, or trusted
28
+ * under a spelling the filesystem never confirmed. This form throws instead, leaving the caller
29
+ * to decide what an unresolvable path means.
30
+ */
31
+ export function canonicalizePathStrict(path) {
32
+ return realpathSync.native(path);
33
+ }
34
+ /** Symlink hops allowed while resolving one path; realpath(3) reports ELOOP past this. */
35
+ const MAX_SYMLINK_HOPS = 40;
36
+ /** Error codes that mean "this component does not exist", which resolution tolerates. */
37
+ const MISSING_PATH_CODES = new Set(["ENOENT", "ENOTDIR"]);
38
+ function splitParts(pathSegment) {
39
+ return pathSegment.split(sep).filter((part) => part.length > 0);
40
+ }
41
+ /**
42
+ * realpath(3) semantics with one lstat/readlink per component and no open(2) anywhere.
43
+ *
44
+ * `..` is applied to the already-resolved prefix rather than collapsed lexically, because the two
45
+ * differ whenever a link target reaches back through a symlink: for `entry -> jump/../secret` with
46
+ * `jump -> /elsewhere/dir`, POSIX resolves `jump` first and lands in `/elsewhere/secret`, while a
47
+ * lexical collapse would answer `secret` next to `entry`. Handing a lexical answer to a
48
+ * containment decision would approve one directory while the I/O reached another, so neither the
49
+ * input nor a link target is normalized before traversal.
50
+ */
51
+ function resolveWithoutOpen(inputPath) {
52
+ const absolutePath = isAbsolute(inputPath) ? inputPath : nodeResolvePath(inputPath);
53
+ const { root } = parse(absolutePath);
54
+ const pending = splitParts(absolutePath.slice(root.length));
55
+ const resolved = [];
56
+ let hops = 0;
57
+ while (pending.length > 0) {
58
+ const part = pending.shift();
59
+ if (part === undefined)
60
+ break;
61
+ if (part === ".")
62
+ continue;
63
+ if (part === "..") {
64
+ // At the root `..` is the root itself, which an empty stack already represents.
65
+ resolved.pop();
66
+ continue;
67
+ }
68
+ const candidate = join(root, ...resolved, part);
69
+ let link;
70
+ try {
71
+ if (!lstatSync(candidate).isSymbolicLink()) {
72
+ resolved.push(part);
73
+ continue;
74
+ }
75
+ hops += 1;
76
+ if (hops > MAX_SYMLINK_HOPS)
77
+ return { path: join(candidate, ...pending), errorCode: "ELOOP" };
78
+ link = readlinkSync(candidate);
79
+ }
80
+ catch (error) {
81
+ const code = error?.code ?? "UNKNOWN";
82
+ return { path: join(candidate, ...pending), errorCode: code };
83
+ }
84
+ if (isAbsolute(link)) {
85
+ resolved.length = 0;
86
+ pending.unshift(...splitParts(link.slice(parse(link).root.length)));
87
+ }
88
+ else {
89
+ pending.unshift(...splitParts(link));
90
+ }
91
+ }
92
+ return { path: join(root, ...resolved) };
93
+ }
94
+ /**
95
+ * Resolve symlinks without ever open(2)-ing a component, tolerating every error.
96
+ *
97
+ * Bun's `fs.realpath*` opens every directory it resolves, so an autofs trigger such as macOS
98
+ * `/home` blocks the calling thread (on the host main thread that freezes the TUI) and an
99
+ * execute-only directory fails with EACCES; lstat needs only search permission and never mounts
100
+ * anything. Components from the first unresolvable one onward are kept verbatim, so a file that
101
+ * does not exist yet still lands where its symlinked parent points.
102
+ *
103
+ * Never throws, so the resolved path may be incomplete. Use it where an approximate answer is
104
+ * better than a blocked thread — the external-path classifier and the monitor parent identity —
105
+ * and use {@link realpathWithoutOpenStrict} where an incomplete answer must not be trusted.
106
+ */
107
+ export function realpathWithoutOpen(inputPath) {
108
+ return resolveWithoutOpen(inputPath).path;
109
+ }
110
+ /**
111
+ * {@link realpathWithoutOpen} that refuses to answer with a guess: a missing component is still
112
+ * tolerated (its name is appended verbatim, which is what a not-yet-created target needs), but
113
+ * EACCES, EIO, ELOOP and hop exhaustion throw instead of returning an unresolved path. Callers
114
+ * that feed a canonical path into a containment or identity decision need this contract, because
115
+ * an unresolved path can name a different file than the one the I/O will reach.
116
+ */
117
+ export function realpathWithoutOpenStrict(inputPath) {
118
+ const resolution = resolveWithoutOpen(inputPath);
119
+ if (resolution.errorCode !== undefined && !MISSING_PATH_CODES.has(resolution.errorCode)) {
120
+ throw Object.assign(new Error(`${resolution.errorCode}: cannot resolve path '${inputPath}' without opening it`), {
121
+ code: resolution.errorCode,
122
+ path: inputPath,
123
+ });
124
+ }
125
+ return resolution.path;
126
+ }
21
127
  export function getFileRevision(path) {
22
128
  try {
23
129
  const stats = statSync(path, { bigint: true });
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAelE;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,IAAI,CAAC;QACJ,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC3C,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,OAAO,GAAG,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,wGAAwG;IACxG,IACC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAC1B,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAC1B,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;QAC7B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;QAC3B,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;QAC5B,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EACzB,CAAC;QACF,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACzD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IACvG,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC7E,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/C,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,MAAM,IAAI,EAAE,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,UAA4B,EAAE;IAC1E,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACrD,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACpC,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzD,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,UAAU,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;QAC1C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnG,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,UAAU,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,UAAkB,OAAO,CAAC,GAAG,EAAE,EAAE,UAA4B,EAAE;IACzG,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;AAC9G,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,GAAW;IAC/D,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IACzD,MAAM,WAAW,GAChB,YAAY,KAAK,EAAE;QACnB,CAAC,YAAY,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;IAE9F,OAAO,WAAW,CAAC,CAAC,CAAC,YAAY,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,iCAAiC,CAAC,QAAgB,EAAE,GAAW;IAC9E,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,CAAC,kBAAkB,CAAC,YAAY,EAAE,GAAG,CAAC,IAAI,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAChE,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAY;IACtD,MAAM,KAAK,GACV,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC5B,CAAC,CAAC,CAAC,qBAAqB,EAAE,iCAAiC,CAAC;QAC5D,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC7B,CAAC,CAAC,CAAC,0BAA0B,CAAC;YAC9B,CAAC,CAAC,EAAE,CAAC;IAER,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACnC,gBAAgB,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC5F,CAAC;aAAM,CAAC;YACP,gBAAgB,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrG,CAAC;IACF,CAAC;AACF,CAAC","sourcesContent":["import { realpathSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve as nodeResolvePath, relative, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { spawnProcessSync } from \"./child-process.ts\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nexport interface PathInputOptions {\n\t/** Trim leading/trailing whitespace before normalization. */\n\ttrim?: boolean;\n\t/** Expand leading `~` to a home directory. Defaults to true. */\n\texpandTilde?: boolean;\n\t/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */\n\thomeDir?: string;\n\t/** Strip a leading `@`, used for CLI @file paths. */\n\tstripAtPrefix?: boolean;\n\t/** Normalize unicode space variants to regular spaces. */\n\tnormalizeUnicodeSpaces?: boolean;\n}\n\n/**\n * Resolve a path to its canonical (real) form, following symlinks.\n * Falls back to the raw path if resolution fails (e.g. the target does\n * not exist yet), so that callers never crash on missing filesystem\n * entries.\n */\nexport function canonicalizePath(path: string): string {\n\ttry {\n\t\treturn realpathSync(path);\n\t} catch {\n\t\treturn path;\n\t}\n}\n\nexport function getFileRevision(path: string): string | undefined {\n\ttry {\n\t\tconst stats = statSync(path, { bigint: true });\n\t\treturn `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}:${stats.ctimeNs}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Returns true if the value is NOT a package source (npm:, git:, etc.)\n * or a remote URL protocol. Bare names, relative paths, and file: URLs\n * are considered local.\n */\nexport function isLocalPath(value: string): boolean {\n\tconst trimmed = value.trim();\n\t// Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath().\n\tif (\n\t\ttrimmed.startsWith(\"npm:\") ||\n\t\ttrimmed.startsWith(\"git:\") ||\n\t\ttrimmed.startsWith(\"github:\") ||\n\t\ttrimmed.startsWith(\"http:\") ||\n\t\ttrimmed.startsWith(\"https:\") ||\n\t\ttrimmed.startsWith(\"ssh:\")\n\t) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n/** Convert Git Bash, MSYS, Cygwin, and WSL drive paths to a form native Windows APIs accept. */\nexport function normalizeWindowsShellPath(filePath: string): string {\n\tif (!filePath.startsWith(\"/\") || filePath.startsWith(\"//\") || filePath.includes(\"\\\\\")) return filePath;\n\tconst match = filePath.match(/^\\/(?:mnt\\/|cygdrive\\/)?([a-z])(?:\\/(.*))?$/i);\n\tif (!match) return filePath;\n\tconst suffix = match[2]?.replaceAll(\"/\", \"\\\\\");\n\treturn `${match[1].toUpperCase()}:\\\\${suffix ?? \"\"}`;\n}\n\nexport function normalizePath(input: string, options: PathInputOptions = {}): string {\n\tlet normalized = options.trim ? input.trim() : input;\n\tif (options.normalizeUnicodeSpaces) {\n\t\tnormalized = normalized.replace(UNICODE_SPACES, \" \");\n\t}\n\tif (options.stripAtPrefix && normalized.startsWith(\"@\")) {\n\t\tnormalized = normalized.slice(1);\n\t}\n\tif (process.platform === \"win32\") {\n\t\tnormalized = normalizeWindowsShellPath(normalized);\n\t}\n\n\tif (options.expandTilde ?? true) {\n\t\tconst home = options.homeDir ?? homedir();\n\t\tif (normalized === \"~\") return home;\n\t\tif (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\t\treturn join(home, normalized.slice(2));\n\t\t}\n\t}\n\n\tif (/^file:\\/\\//.test(normalized)) {\n\t\treturn fileURLToPath(normalized);\n\t}\n\n\treturn normalized;\n}\n\nexport function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {\n\tconst normalized = normalizePath(input, options);\n\tconst normalizedBaseDir = normalizePath(baseDir);\n\treturn isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);\n}\n\nexport function getCwdRelativePath(filePath: string, cwd: string): string | undefined {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedPath = resolvePath(filePath, resolvedCwd);\n\tconst relativePath = relative(resolvedCwd, resolvedPath);\n\tconst isInsideCwd =\n\t\trelativePath === \"\" ||\n\t\t(relativePath !== \"..\" && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));\n\n\treturn isInsideCwd ? relativePath || \".\" : undefined;\n}\n\nexport function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string {\n\tconst absolutePath = resolvePath(filePath, cwd);\n\treturn (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join(\"/\");\n}\n\nexport function shortenPath(path: string): string {\n\tif (!path) return path;\n\tconst home = homedir();\n\tif (path.startsWith(home)) return `~${path.slice(home.length)}`;\n\treturn path;\n}\n\nexport function markPathIgnoredByCloudSync(path: string): void {\n\tconst attrs =\n\t\tprocess.platform === \"darwin\"\n\t\t\t? [\"com.dropbox.ignored\", \"com.apple.fileprovider.ignore#P\"]\n\t\t\t: process.platform === \"linux\"\n\t\t\t\t? [\"user.com.dropbox.ignored\"]\n\t\t\t\t: [];\n\n\tfor (const attr of attrs) {\n\t\tif (process.platform === \"darwin\") {\n\t\t\tspawnProcessSync(\"xattr\", [\"-w\", attr, \"1\", path], { encoding: \"utf-8\", stdio: \"ignore\" });\n\t\t} else {\n\t\t\tspawnProcessSync(\"setfattr\", [\"-n\", attr, \"-v\", \"1\", path], { encoding: \"utf-8\", stdio: \"ignore\" });\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAelE;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,IAAI,CAAC;QACJ,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IAClD,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,0FAA0F;AAC1F,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,yFAAyF;AACzF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AAE1D,SAAS,UAAU,CAAC,WAAmB;IACtC,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjE,CAAC;AASD;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,SAAiB;IAC5C,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IACpF,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM;QAC9B,IAAI,IAAI,KAAK,GAAG;YAAE,SAAS;QAC3B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACnB,gFAAgF;YAChF,QAAQ,CAAC,GAAG,EAAE,CAAC;YACf,SAAS;QACV,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAC;QAChD,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACJ,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC5C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,SAAS;YACV,CAAC;YACD,IAAI,IAAI,CAAC,CAAC;YACV,IAAI,IAAI,GAAG,gBAAgB;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;YAC9F,IAAI,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,GAAI,KAA+B,EAAE,IAAI,IAAI,SAAS,CAAC;YACjE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAC/D,CAAC;QACD,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACpB,OAAO,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACrE,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,CAAC;IACF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACpD,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IAC1D,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACjD,IAAI,UAAU,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACzF,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,UAAU,CAAC,SAAS,0BAA0B,SAAS,sBAAsB,CAAC,EAAE;YAChH,IAAI,EAAE,UAAU,CAAC,SAAS;YAC1B,IAAI,EAAE,SAAS;SACf,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC3C,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,OAAO,GAAG,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,wGAAwG;IACxG,IACC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAC1B,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAC1B,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;QAC7B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;QAC3B,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;QAC5B,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EACzB,CAAC;QACF,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACzD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IACvG,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC7E,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/C,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,MAAM,IAAI,EAAE,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,UAA4B,EAAE;IAC1E,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACrD,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACpC,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzD,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,UAAU,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;QAC1C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnG,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,UAAU,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,UAAkB,OAAO,CAAC,GAAG,EAAE,EAAE,UAA4B,EAAE;IACzG,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;AAC9G,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,GAAW;IAC/D,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IACzD,MAAM,WAAW,GAChB,YAAY,KAAK,EAAE;QACnB,CAAC,YAAY,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;IAE9F,OAAO,WAAW,CAAC,CAAC,CAAC,YAAY,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,iCAAiC,CAAC,QAAgB,EAAE,GAAW;IAC9E,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,CAAC,kBAAkB,CAAC,YAAY,EAAE,GAAG,CAAC,IAAI,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAChE,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAY;IACtD,MAAM,KAAK,GACV,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC5B,CAAC,CAAC,CAAC,qBAAqB,EAAE,iCAAiC,CAAC;QAC5D,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC7B,CAAC,CAAC,CAAC,0BAA0B,CAAC;YAC9B,CAAC,CAAC,EAAE,CAAC;IAER,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACnC,gBAAgB,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC5F,CAAC;aAAM,CAAC;YACP,gBAAgB,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrG,CAAC;IACF,CAAC;AACF,CAAC","sourcesContent":["import { lstatSync, readlinkSync, realpathSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve as nodeResolvePath, parse, relative, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { spawnProcessSync } from \"./child-process.ts\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nexport interface PathInputOptions {\n\t/** Trim leading/trailing whitespace before normalization. */\n\ttrim?: boolean;\n\t/** Expand leading `~` to a home directory. Defaults to true. */\n\texpandTilde?: boolean;\n\t/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */\n\thomeDir?: string;\n\t/** Strip a leading `@`, used for CLI @file paths. */\n\tstripAtPrefix?: boolean;\n\t/** Normalize unicode space variants to regular spaces. */\n\tnormalizeUnicodeSpaces?: boolean;\n}\n\n/**\n * Resolve a path to its canonical (real) form, following symlinks.\n * Falls back to the raw path if resolution fails (e.g. the target does\n * not exist yet), so that callers never crash on missing filesystem\n * entries.\n */\nexport function canonicalizePath(path: string): string {\n\ttry {\n\t\treturn realpathSync.native(path);\n\t} catch {\n\t\treturn path;\n\t}\n}\n\n/**\n * {@link canonicalizePath} that refuses to guess.\n *\n * The convenience form hands back its input when resolution fails, which is what most callers\n * want: they compare two canonical spellings and an unresolvable path simply compares unequal.\n * A security decision cannot use that, because the raw input is indistinguishable from a\n * canonical answer - so a path that could not be resolved would be keyed, compared, or trusted\n * under a spelling the filesystem never confirmed. This form throws instead, leaving the caller\n * to decide what an unresolvable path means.\n */\nexport function canonicalizePathStrict(path: string): string {\n\treturn realpathSync.native(path);\n}\n\n/** Symlink hops allowed while resolving one path; realpath(3) reports ELOOP past this. */\nconst MAX_SYMLINK_HOPS = 40;\n\n/** Error codes that mean \"this component does not exist\", which resolution tolerates. */\nconst MISSING_PATH_CODES = new Set([\"ENOENT\", \"ENOTDIR\"]);\n\nfunction splitParts(pathSegment: string): string[] {\n\treturn pathSegment.split(sep).filter((part) => part.length > 0);\n}\n\ninterface OpenFreeResolution {\n\t/** The resolved path; components from an unresolvable one onward are kept verbatim. */\n\treadonly path: string;\n\t/** Set when resolution stopped early: the fs error code, or ELOOP for hop exhaustion. */\n\treadonly errorCode?: string;\n}\n\n/**\n * realpath(3) semantics with one lstat/readlink per component and no open(2) anywhere.\n *\n * `..` is applied to the already-resolved prefix rather than collapsed lexically, because the two\n * differ whenever a link target reaches back through a symlink: for `entry -> jump/../secret` with\n * `jump -> /elsewhere/dir`, POSIX resolves `jump` first and lands in `/elsewhere/secret`, while a\n * lexical collapse would answer `secret` next to `entry`. Handing a lexical answer to a\n * containment decision would approve one directory while the I/O reached another, so neither the\n * input nor a link target is normalized before traversal.\n */\nfunction resolveWithoutOpen(inputPath: string): OpenFreeResolution {\n\tconst absolutePath = isAbsolute(inputPath) ? inputPath : nodeResolvePath(inputPath);\n\tconst { root } = parse(absolutePath);\n\tconst pending = splitParts(absolutePath.slice(root.length));\n\tconst resolved: string[] = [];\n\tlet hops = 0;\n\twhile (pending.length > 0) {\n\t\tconst part = pending.shift();\n\t\tif (part === undefined) break;\n\t\tif (part === \".\") continue;\n\t\tif (part === \"..\") {\n\t\t\t// At the root `..` is the root itself, which an empty stack already represents.\n\t\t\tresolved.pop();\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidate = join(root, ...resolved, part);\n\t\tlet link: string;\n\t\ttry {\n\t\t\tif (!lstatSync(candidate).isSymbolicLink()) {\n\t\t\t\tresolved.push(part);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\thops += 1;\n\t\t\tif (hops > MAX_SYMLINK_HOPS) return { path: join(candidate, ...pending), errorCode: \"ELOOP\" };\n\t\t\tlink = readlinkSync(candidate);\n\t\t} catch (error) {\n\t\t\tconst code = (error as NodeJS.ErrnoException)?.code ?? \"UNKNOWN\";\n\t\t\treturn { path: join(candidate, ...pending), errorCode: code };\n\t\t}\n\t\tif (isAbsolute(link)) {\n\t\t\tresolved.length = 0;\n\t\t\tpending.unshift(...splitParts(link.slice(parse(link).root.length)));\n\t\t} else {\n\t\t\tpending.unshift(...splitParts(link));\n\t\t}\n\t}\n\treturn { path: join(root, ...resolved) };\n}\n\n/**\n * Resolve symlinks without ever open(2)-ing a component, tolerating every error.\n *\n * Bun's `fs.realpath*` opens every directory it resolves, so an autofs trigger such as macOS\n * `/home` blocks the calling thread (on the host main thread that freezes the TUI) and an\n * execute-only directory fails with EACCES; lstat needs only search permission and never mounts\n * anything. Components from the first unresolvable one onward are kept verbatim, so a file that\n * does not exist yet still lands where its symlinked parent points.\n *\n * Never throws, so the resolved path may be incomplete. Use it where an approximate answer is\n * better than a blocked thread — the external-path classifier and the monitor parent identity —\n * and use {@link realpathWithoutOpenStrict} where an incomplete answer must not be trusted.\n */\nexport function realpathWithoutOpen(inputPath: string): string {\n\treturn resolveWithoutOpen(inputPath).path;\n}\n\n/**\n * {@link realpathWithoutOpen} that refuses to answer with a guess: a missing component is still\n * tolerated (its name is appended verbatim, which is what a not-yet-created target needs), but\n * EACCES, EIO, ELOOP and hop exhaustion throw instead of returning an unresolved path. Callers\n * that feed a canonical path into a containment or identity decision need this contract, because\n * an unresolved path can name a different file than the one the I/O will reach.\n */\nexport function realpathWithoutOpenStrict(inputPath: string): string {\n\tconst resolution = resolveWithoutOpen(inputPath);\n\tif (resolution.errorCode !== undefined && !MISSING_PATH_CODES.has(resolution.errorCode)) {\n\t\tthrow Object.assign(new Error(`${resolution.errorCode}: cannot resolve path '${inputPath}' without opening it`), {\n\t\t\tcode: resolution.errorCode,\n\t\t\tpath: inputPath,\n\t\t});\n\t}\n\treturn resolution.path;\n}\n\nexport function getFileRevision(path: string): string | undefined {\n\ttry {\n\t\tconst stats = statSync(path, { bigint: true });\n\t\treturn `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}:${stats.ctimeNs}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Returns true if the value is NOT a package source (npm:, git:, etc.)\n * or a remote URL protocol. Bare names, relative paths, and file: URLs\n * are considered local.\n */\nexport function isLocalPath(value: string): boolean {\n\tconst trimmed = value.trim();\n\t// Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath().\n\tif (\n\t\ttrimmed.startsWith(\"npm:\") ||\n\t\ttrimmed.startsWith(\"git:\") ||\n\t\ttrimmed.startsWith(\"github:\") ||\n\t\ttrimmed.startsWith(\"http:\") ||\n\t\ttrimmed.startsWith(\"https:\") ||\n\t\ttrimmed.startsWith(\"ssh:\")\n\t) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n/** Convert Git Bash, MSYS, Cygwin, and WSL drive paths to a form native Windows APIs accept. */\nexport function normalizeWindowsShellPath(filePath: string): string {\n\tif (!filePath.startsWith(\"/\") || filePath.startsWith(\"//\") || filePath.includes(\"\\\\\")) return filePath;\n\tconst match = filePath.match(/^\\/(?:mnt\\/|cygdrive\\/)?([a-z])(?:\\/(.*))?$/i);\n\tif (!match) return filePath;\n\tconst suffix = match[2]?.replaceAll(\"/\", \"\\\\\");\n\treturn `${match[1].toUpperCase()}:\\\\${suffix ?? \"\"}`;\n}\n\nexport function normalizePath(input: string, options: PathInputOptions = {}): string {\n\tlet normalized = options.trim ? input.trim() : input;\n\tif (options.normalizeUnicodeSpaces) {\n\t\tnormalized = normalized.replace(UNICODE_SPACES, \" \");\n\t}\n\tif (options.stripAtPrefix && normalized.startsWith(\"@\")) {\n\t\tnormalized = normalized.slice(1);\n\t}\n\tif (process.platform === \"win32\") {\n\t\tnormalized = normalizeWindowsShellPath(normalized);\n\t}\n\n\tif (options.expandTilde ?? true) {\n\t\tconst home = options.homeDir ?? homedir();\n\t\tif (normalized === \"~\") return home;\n\t\tif (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\t\treturn join(home, normalized.slice(2));\n\t\t}\n\t}\n\n\tif (/^file:\\/\\//.test(normalized)) {\n\t\treturn fileURLToPath(normalized);\n\t}\n\n\treturn normalized;\n}\n\nexport function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {\n\tconst normalized = normalizePath(input, options);\n\tconst normalizedBaseDir = normalizePath(baseDir);\n\treturn isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);\n}\n\nexport function getCwdRelativePath(filePath: string, cwd: string): string | undefined {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedPath = resolvePath(filePath, resolvedCwd);\n\tconst relativePath = relative(resolvedCwd, resolvedPath);\n\tconst isInsideCwd =\n\t\trelativePath === \"\" ||\n\t\t(relativePath !== \"..\" && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));\n\n\treturn isInsideCwd ? relativePath || \".\" : undefined;\n}\n\nexport function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string {\n\tconst absolutePath = resolvePath(filePath, cwd);\n\treturn (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join(\"/\");\n}\n\nexport function shortenPath(path: string): string {\n\tif (!path) return path;\n\tconst home = homedir();\n\tif (path.startsWith(home)) return `~${path.slice(home.length)}`;\n\treturn path;\n}\n\nexport function markPathIgnoredByCloudSync(path: string): void {\n\tconst attrs =\n\t\tprocess.platform === \"darwin\"\n\t\t\t? [\"com.dropbox.ignored\", \"com.apple.fileprovider.ignore#P\"]\n\t\t\t: process.platform === \"linux\"\n\t\t\t\t? [\"user.com.dropbox.ignored\"]\n\t\t\t\t: [];\n\n\tfor (const attr of attrs) {\n\t\tif (process.platform === \"darwin\") {\n\t\t\tspawnProcessSync(\"xattr\", [\"-w\", attr, \"1\", path], { encoding: \"utf-8\", stdio: \"ignore\" });\n\t\t} else {\n\t\t\tspawnProcessSync(\"setfattr\", [\"-n\", attr, \"-v\", \"1\", path], { encoding: \"utf-8\", stdio: \"ignore\" });\n\t\t}\n\t}\n}\n"]}
@@ -136,7 +136,7 @@ Configure compaction in `~/.senpi/agent/settings.json` (global) or `.senpi/setti
136
136
 
137
137
  | Setting | Default | Description |
138
138
  |---------|---------|-------------|
139
- | `compaction.enabled` | `true` | Enable auto-compaction. Set to `false` for manual control only. |
139
+ | `compaction.enabled` | `true` | Enable proactive auto-compaction. `false` leaves threshold compaction to you; a provider-rejected context overflow still runs the one-shot compact-and-retry recovery. |
140
140
  | `compaction.reserveTokens` | `16384` | Tokens reserved for the LLM's response. Lower values allow more context but risk truncation. |
141
141
  | `compaction.keepRecentTokens` | `20000` | How much recent conversation to keep uncompressed. Higher values preserve more context but trigger compaction sooner. |
142
142
 
@@ -221,7 +221,7 @@ For technical details, see:
221
221
 
222
222
  ### "I want to disable auto-compaction"
223
223
 
224
- Set `compaction.enabled: false` in your settings. Use `/compact` manually when you want to compact. Be aware that without auto-compaction, long sessions may hit context limits and fail.
224
+ Set `compaction.enabled: false` in your settings. Use `/compact` manually when you want to compact. Threshold compaction stays off, but a context the provider rejects as too large is still compacted once and retried so the session does not stall. The RPC `set_auto_compaction` command and OmO Desktop's per-thread toggle change only that session; they do not write this setting.
225
225
 
226
226
  ### "How do I see what was compacted?"
227
227
 
@@ -411,8 +411,8 @@ Configure compaction in `~/.senpi/agent/settings.json` or `<project-dir>/.senpi/
411
411
 
412
412
  | Setting | Default | Description |
413
413
  |---------|---------|-------------|
414
- | `enabled` | `true` | Enable auto-compaction |
414
+ | `enabled` | `true` | Enable proactive (threshold) auto-compaction; provider-overflow recovery stays armed |
415
415
  | `reserveTokens` | `16384` | Tokens to reserve for LLM response |
416
416
  | `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |
417
417
 
418
- Disable auto-compaction with `"enabled": false`. You can still compact manually with `/compact`.
418
+ Disable proactive auto-compaction with `"enabled": false`. You can still compact manually with `/compact`, and a context the provider rejects as too large is still compacted once and retried automatically.
@@ -62,10 +62,11 @@ bun run check # Biome + tsc + browser-smoke check (pre-commit equivalent)
62
62
 
63
63
  Live-API tests are env-gated vitest tests. Set `PI_ENABLE_LIVE_API_TESTS=1` (or a per-provider flag from `packages/ai/test/live-api-gates.ts`) plus the provider API keys, then run `bun run test`.
64
64
 
65
- Run a specific test:
65
+ Run a specific test from the package, or from the repository root through the workspace runner (the root form runs the `scripts/` tests first):
66
66
 
67
67
  ```bash
68
- bun run test --workspace @code-yeongyu/senpi -- test/specific.test.ts
68
+ bun run --cwd packages/coding-agent test -- test/specific.test.ts
69
+ bun run test --workspace packages/coding-agent -- test/specific.test.ts
69
70
  ```
70
71
 
71
72
  ## Project Structure
package/docs/providers.md CHANGED
@@ -101,7 +101,7 @@ If your Claude Pro/Max subscription usage through `claude-sdk-oauth` feels unexp
101
101
  | Anthropic-compatible providers (kimi-coding, fireworks, gateways) | 5 minutes | The 1h TTL is gated on the native `api.anthropic.com` base URL, so these lanes stay short. | `cacheRetention` |
102
102
 
103
103
  Override precedence: `cacheRetention` in `models.json` / the model catalog wins over everything. `PI_CACHE_RETENTION=long` selects long; any other set value forces short; unset falls back to the lane default above.
104
- 5. **Goal-monitor timing.** The goal monitor's continuation backstop is derived from the model's cache-safe wait (TTL minus `promptCache.safetyBufferSeconds`, default 30), capped by `promptCache.goalBackstopMaxSeconds` (default 3570), instead of a fixed 4 minutes. The default 5m lanes wake every ~4m30s; a supported lane explicitly configured for 1h retention can wait up to 59m30s. Cache-warm notices show which warm iteration you are on.
104
+ 5. **Goal-monitor timing.** While at least one wake source is live, the goal monitor arms one stall backstop of `promptCache.goalBackstopMaxSeconds` (default 3570, so ~59m30s, hard-capped at 1h) and nothing else. It no longer wakes on the cache-safe wait (TTL minus `promptCache.safetyBufferSeconds`), because each of those wakes was a full main-model turn that re-sent the accumulated context every ~4m30s for the whole wait. The normal resumption is event-driven: when the last wake source drains, exactly one continuation is queued about a second later. The backstop only fires if nothing ever delivers, as a stall safety net; the prompt cache is expected to have expired by then, and `promptCache.keepAlive` (opt-in, off by default) is the only thing that refreshes it during the wait. Cache-warm notices show which warm iteration you are on.
105
105
  6. **Wake sources that hold the goal backstop.** Anything that can wake a parked session publishes a `wake_source_state` event (`{source, activeCount}`): terminal monitors (`terminal-monitors`), background bash sessions including auto-detached and killed ones (`terminal-background-sessions`), detached `eval` cells (`senpi-codemode`), and omo-senpi background task children plus owned team members (`senpi-task`). The goal extension sums every source, so a goal waits inside the prompt-cache TTL while ANY of them is on duty instead of continuing immediately. The legacy `terminal_monitor_state` event is still emitted for external consumers and is folded onto the same `terminal-monitors` count.
106
106
  7. **Directive-block deduplication.** As of v2026.8.4, the flatten serialization collapses repeated `<ultrawork-mode>` directive blocks to a single copy, preventing the issue-#494 scenario where duplicated ~17KB directive blocks consumed up to 73% of the re-sent prompt. The continuity observation reports how many were collapsed and the payload size.
107
107
 
package/docs/rpc.md CHANGED
@@ -795,7 +795,7 @@ Response:
795
795
 
796
796
  #### set_auto_compaction
797
797
 
798
- Enable or disable automatic compaction when context is nearly full.
798
+ Enable or disable automatic compaction when context is nearly full for this session only. The persisted `compaction.enabled` setting is left untouched, and `get_state` reports the effective value. Disabling it stops proactive threshold compaction; a provider-rejected context overflow still triggers the one-shot compact-and-retry recovery.
799
799
 
800
800
  ```json
801
801
  {"type": "set_auto_compaction", "enabled": true}
@@ -12,6 +12,35 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.7-2] - 2026-09-07
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - The JS kernel's shell capture now pins the worker's environment view for `Bun.spawnSync` as well as `Bun.spawn`, so a cell calling it without an explicit `env` sees the session's `PI_*` values instead of the inherited OS environ.
26
+ - Eval kernels and every child they spawn now see the active session's `PI_*` environment (`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, `PI_REASONING_LEVEL`) exactly as bash-tool children do: inherited `PI_*` values are dropped before the session values are applied, so subprocesses such as `omo-agent-toolkit ulw-loop` resolve the same session as the `bash` tool instead of a cwd-global one.
27
+ - JavaScript eval cells no longer lose their completion value when a nested function, callback, or try/catch helper contains `return`: the cell wrapper now skips last-expression capture only for a genuine top-level `return`, and a property named `return` no longer primes the statement scanner as the keyword (#1439).
28
+ - Eval output truncation notices now name the real cause: a width-clamped line reports `N line(s) clamped to M columns (… dropped)`, a byte-capped tail reports the actual cap, and a notice never presents the output's own size as a limit.
29
+
30
+ ### Removed
31
+
32
+ ## [2026.9.7] - 2026-09-07
33
+
34
+ ### Breaking Changes
35
+
36
+ ### Added
37
+
38
+ ### Changed
39
+
40
+ ### Fixed
41
+
42
+ ### Removed
43
+
15
44
  ## [2026.9.6] - 2026-09-06
16
45
 
17
46
  ### Breaking Changes
@@ -54,6 +54,19 @@ task-tool names are known.
54
54
  A missing optional interpreter removes that language from the session's `eval`
55
55
  schema; it is not an installation failure.
56
56
 
57
+ ### Session environment
58
+
59
+ Every kernel starts with the active session's `PI_*` environment — `PI_SESSION_ID`,
60
+ `PI_SESSION_FILE` (when the session is persistent), `PI_PROVIDER`, `PI_MODEL`, and
61
+ `PI_REASONING_LEVEL` (when set) — resolved at session start, mirroring the bash tool's
62
+ session environment contract. The values are visible to `env()`/`process.env`/`os.environ`
63
+ inside cells and are inherited by every child process a cell spawns
64
+ (`Bun.$`, `Bun.spawn`, `child_process`, `subprocess`, ...). Inherited `PI_*` values from
65
+ the launching environment are dropped first, so a child spawned from a cell sees exactly
66
+ what a child spawned from the bash tool sees. The values snapshot at kernel start, so a
67
+ mid-session model switch updates the bash tool's next command but not already-running
68
+ kernels; a new session starts fresh kernels with fresh values.
69
+
57
70
  ## Settings
58
71
 
59
72
  Configuration is loaded in this order:
@@ -116,7 +129,7 @@ options object and asynchronous helpers are `await`-able.
116
129
  | `print(value, ...)` | Emits text output. |
117
130
  | `read(path, offset?, limit?)` | Reads text with 1-indexed line slicing. `local://` paths resolve under the session artifact root. |
118
131
  | `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
119
- | `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. |
132
+ | `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
120
133
  | `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
121
134
  | `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
122
135
  | `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.6",
3
+ "version": "2026.9.7-2",
4
4
  "private": true,
5
5
  "description": "Source-only senpi extension package for codemode evaluation tools",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@babel/parser": "8.0.4",
34
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.6",
34
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.7-2",
35
35
  "typebox": "1.3.18"
36
36
  },
37
37
  "peerDependencies": {
38
- "@code-yeongyu/senpi": "2026.9.6"
38
+ "@code-yeongyu/senpi": "2026.9.7-2"
39
39
  },
40
40
  "devDependencies": {
41
- "@code-yeongyu/senpi": "2026.9.6"
41
+ "@code-yeongyu/senpi": "2026.9.7-2"
42
42
  },
43
43
  "keywords": [
44
44
  "senpi",
@@ -26,6 +26,7 @@ const hostToKernelMessageSchema = Type.Union([
26
26
  type: Type.Literal("init"),
27
27
  sessionId: Type.String({ minLength: 1 }),
28
28
  connection: connectionConfigSchema,
29
+ sessionEnv: Type.Optional(Type.Record(Type.String(), Type.String())),
29
30
  }),
30
31
  Type.Object({
31
32
  type: Type.Literal("run"),
@@ -13,6 +13,7 @@ import {
13
13
  getInterpreterAvailability,
14
14
  type InterpreterAvailability,
15
15
  } from "../interpreters/detect.ts";
16
+ import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
16
17
  import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
17
18
  import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
18
19
  import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
@@ -66,11 +67,13 @@ export async function createRuntime(
66
67
  const executeTool = createExecuteTool(pi, activeTools);
67
68
  const create = options.createSessionManager ?? createCodemodeSessionManager;
68
69
  const sessionId = sessionIdFrom(event);
70
+ const sessionEnv = sessionEnvironmentFrom(ctx);
69
71
  const configuredPoolWidth = settings.parallelPoolWidth;
70
72
  const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
71
73
  const manager = await create({
72
74
  sessionId,
73
75
  cwd: ctx.cwd,
76
+ sessionEnv,
74
77
  settings,
75
78
  availability,
76
79
  artifactsDir: artifacts.dir,