oh-my-opencode 4.18.0 → 4.18.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 (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -1,5 +1,4 @@
1
1
  import { stdin as processStdin } from "node:process";
2
- import { disposeDefaultLspManager } from "@code-yeongyu/lsp-daemon";
3
2
  import { isRecord, runLspPostCompactHook, runLspPostToolUseHook } from "./codex-hook.js";
4
3
  export async function runPostToolUseHookCli(stdin = processStdin) {
5
4
  await runHookCli((input) => runLspPostToolUseHook(input), stdin);
@@ -22,9 +21,6 @@ async function runHookCli(runHook, stdin) {
22
21
  // LSP feedback is best-effort: stderr or a non-zero exit here surfaces as harness noise on every edit.
23
22
  return;
24
23
  }
25
- finally {
26
- await disposeDefaultLspManager().catch(() => undefined);
27
- }
28
24
  }
29
25
  async function readStdin(stdin) {
30
26
  stdin.setEncoding("utf8");
@@ -1,5 +1,7 @@
1
+ import { type PostEditDiagnosticsOutcome } from "@oh-my-opencode/lsp-core/post-edit";
2
+ import { type LspRequestContext } from "@oh-my-opencode/lsp-core/request-context";
1
3
  export { extractMutatedFilePaths } from "./mutated-file-paths.js";
2
- export type DiagnosticsRunner = (filePath: string) => Promise<string>;
4
+ export type DiagnosticsRunner = (filePath: string) => Promise<PostEditDiagnosticsOutcome>;
3
5
  export interface CodexPostToolUseInput {
4
6
  session_id?: unknown;
5
7
  tool_name?: unknown;
@@ -10,7 +12,8 @@ export interface CodexPostToolUseInput {
10
12
  export interface CodexPostCompactInput {
11
13
  session_id?: unknown;
12
14
  }
13
- export declare function runLspDiagnosticsText(filePath: string): Promise<string>;
15
+ export declare function runLspDiagnosticsText(filePath: string): Promise<PostEditDiagnosticsOutcome>;
16
+ export declare function codexLspRequestContext(env?: Record<string, string | undefined>, cwd?: string): LspRequestContext;
14
17
  export declare function runLspPostToolUseHook(input: CodexPostToolUseInput, runDiagnostics?: DiagnosticsRunner): Promise<string>;
15
18
  export declare function runLspPostCompactHook(input: CodexPostCompactInput): Promise<string>;
16
19
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -1,16 +1,17 @@
1
- import { readFileSync } from "node:fs";
2
- import { callDiagnosticsViaDaemon, currentRequestContext } from "@code-yeongyu/lsp-daemon";
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { callDiagnosticsViaDaemon } from "@code-yeongyu/lsp-daemon/client";
5
+ import { collectPostEditDiagnostics } from "@oh-my-opencode/lsp-core/post-edit";
6
+ import { parseLspRequestContext } from "@oh-my-opencode/lsp-core/request-context";
3
7
  import { ensureLspDaemonCliEnv } from "./daemon-cli-path.js";
4
- import { isLspDaemonUnreachableDiagnostics, isUnavailableLspDiagnostics, markLspSessionCompacted, recordLspDiagnosticsObservations, sessionIdFrom, shouldSkipUnavailableLspDiagnostics, } from "./lsp-session-state.js";
8
+ import { isLspDaemonUnreachableDiagnostics, markLspSessionCompacted, readLspPostEditCache, sessionIdFrom, writeLspPostEditCache, } from "./lsp-session-state.js";
5
9
  import { extractMutatedFilePaths } from "./mutated-file-paths.js";
6
10
  export { extractMutatedFilePaths } from "./mutated-file-paths.js";
7
- const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
8
- const UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
9
11
  const DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
10
12
  const DIAGNOSTIC_CHUNK_PATTERN = /^(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/;
11
13
  const DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
12
14
  const CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
13
- const MAX_CONCURRENT_DIAGNOSTICS = 4;
14
15
  const CONTEXT_PRESSURE_MARKERS = [
15
16
  "context compacted",
16
17
  "context_length_exceeded",
@@ -22,30 +23,46 @@ const CONTEXT_PRESSURE_MARKERS = [
22
23
  ];
23
24
  export async function runLspDiagnosticsText(filePath) {
24
25
  ensureLspDaemonCliEnv();
25
- const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
26
+ const result = await callDiagnosticsViaDaemon(filePath, { context: codexLspRequestContext() });
27
+ return postEditOutcomeFromDaemonResult(result);
28
+ }
29
+ function postEditOutcomeFromDaemonResult(result) {
30
+ const availability = notConfiguredAvailability(result.details);
31
+ if (availability !== undefined)
32
+ return { kind: "not_configured", extension: availability.extension };
26
33
  return result.content.map((block) => block.text).join("\n");
27
34
  }
35
+ function notConfiguredAvailability(details) {
36
+ if (!isRecord(details))
37
+ return undefined;
38
+ const availability = details["availability"];
39
+ if (!isRecord(availability))
40
+ return undefined;
41
+ if (availability["kind"] !== "not_configured")
42
+ return undefined;
43
+ const extension = availability["extension"];
44
+ return typeof extension === "string" && extension.length > 0 ? { extension } : undefined;
45
+ }
46
+ export function codexLspRequestContext(env = process.env, cwd = process.cwd()) {
47
+ const canonicalCwd = realpathSync(resolve(cwd));
48
+ const codexHome = resolve(env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"));
49
+ return parseLspRequestContext({
50
+ cwd: canonicalCwd,
51
+ projectConfigPaths: [join(canonicalCwd, ".codex", "lsp-client.json")],
52
+ userConfigPath: join(codexHome, "lsp-client.json"),
53
+ installDecisionsPath: join(codexHome, "lsp-install-decisions.json"),
54
+ capabilities: { installDecisionTool: true },
55
+ });
56
+ }
28
57
  export async function runLspPostToolUseHook(input, runDiagnostics = runLspDiagnosticsText) {
29
58
  const sessionId = sessionIdFrom(input);
30
- const filePaths = extractMutatedFilePaths(input).filter((filePath) => !shouldSkipUnavailableLspDiagnostics(filePath, sessionId));
59
+ const filePaths = extractMutatedFilePaths(input);
31
60
  if (filePaths.length === 0)
32
61
  return "";
33
- const blocks = [];
34
- const observations = [];
35
- for (const { filePath, diagnostics } of await collectDiagnostics(filePaths, runDiagnostics)) {
36
- // A daemon outage is transient (connect-or-spawn retries on the next request);
37
- // recording it would silence this extension for the rest of the session.
38
- if (isLspDaemonUnreachableDiagnostics(diagnostics))
39
- continue;
40
- const unavailable = isUnavailableLspDiagnostics(diagnostics);
41
- observations.push({ filePath, unavailable });
42
- if (isCleanDiagnostics(diagnostics))
43
- continue;
44
- if (unavailable)
45
- continue;
46
- blocks.push({ filePath, diagnostics });
47
- }
48
- recordLspDiagnosticsObservations(sessionId, observations);
62
+ const cache = readLspPostEditCache(sessionId);
63
+ const result = await collectPostEditDiagnostics({ filePaths, runDiagnostics, cache });
64
+ writeLspPostEditCache(sessionId, cache);
65
+ const blocks = result.blocks.filter(({ diagnostics }) => !isLspDaemonUnreachableDiagnostics(diagnostics));
49
66
  if (blocks.length === 0)
50
67
  return "";
51
68
  const rawReason = blocks.map(formatDiagnosticBlock).join("\n\n");
@@ -64,39 +81,6 @@ export async function runLspPostCompactHook(input) {
64
81
  markLspSessionCompacted(sessionIdFrom(input));
65
82
  return "";
66
83
  }
67
- async function collectDiagnostics(filePaths, runDiagnostics) {
68
- const results = [];
69
- let nextIndex = 0;
70
- const workerCount = Math.min(MAX_CONCURRENT_DIAGNOSTICS, filePaths.length);
71
- async function worker() {
72
- for (;;) {
73
- const index = nextIndex;
74
- nextIndex += 1;
75
- const filePath = filePaths[index];
76
- if (filePath === undefined)
77
- return;
78
- results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
79
- }
80
- }
81
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
82
- return results;
83
- }
84
- async function collectFileDiagnostics(filePath, runDiagnostics) {
85
- try {
86
- return (await runDiagnostics(filePath)).trim();
87
- }
88
- catch (error) {
89
- return formatDiagnosticsError(error);
90
- }
91
- }
92
- function formatDiagnosticsError(error) {
93
- if (error instanceof Error) {
94
- const message = error.message.trim();
95
- if (message.length > 0)
96
- return message;
97
- }
98
- return String(error).trim();
99
- }
100
84
  function formatDiagnosticBlock({ filePath, diagnostics }) {
101
85
  return `LSP diagnostics after editing ${filePath}:\n\n${formatDiagnosticsForDisplay(diagnostics)}`;
102
86
  }
@@ -172,11 +156,6 @@ function limitHookText(text, maxChars) {
172
156
  const head = text.slice(0, maxChars - marker.length).replace(/[ \t\r\n]+$/, "");
173
157
  return `${head}${marker}`;
174
158
  }
175
- function isCleanDiagnostics(diagnostics) {
176
- return (diagnostics.length === 0 ||
177
- diagnostics === CLEAN_DIAGNOSTICS_TEXT ||
178
- diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT));
179
- }
180
159
  export function isRecord(value) {
181
160
  return typeof value === "object" && value !== null && !Array.isArray(value);
182
161
  }
@@ -1,2 +1,2 @@
1
1
  export declare function ensureLspDaemonCliEnv(env?: NodeJS.ProcessEnv): void;
2
- export declare function resolveLspDaemonCliPath(): string;
2
+ export declare function resolveLspDaemonCliPath(env?: NodeJS.ProcessEnv): string;
@@ -2,21 +2,21 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { OMO_LSP_DAEMON_CLI, OMO_LSP_DAEMON_VERSION, resolveDaemonRuntime, } from "@code-yeongyu/lsp-daemon/client";
5
6
  const requireFromHere = createRequire(import.meta.url);
6
- const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
7
- const CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
8
- const CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
7
+ const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/cli";
9
8
  export function ensureLspDaemonCliEnv(env = process.env) {
10
- const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
11
- const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
12
- if (!configuredCli)
13
- env[CODEX_LSP_DAEMON_CLI_ENV] = resolution.cliPath;
14
- if (!env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim() && resolution.version !== null) {
15
- env[CODEX_LSP_DAEMON_VERSION_ENV] = resolution.version;
16
- }
9
+ const runtime = resolveDaemonRuntime(env, resolveLspDaemonCli());
10
+ env[OMO_LSP_DAEMON_CLI] = runtime.cliPath;
11
+ env[OMO_LSP_DAEMON_VERSION] = runtime.version;
17
12
  }
18
- export function resolveLspDaemonCliPath() {
19
- return resolveLspDaemonCli().cliPath;
13
+ export function resolveLspDaemonCliPath(env = process.env) {
14
+ const runtime = resolveDaemonRuntime(env, resolveLspDaemonCli());
15
+ if (env[OMO_LSP_DAEMON_CLI] === undefined)
16
+ env[OMO_LSP_DAEMON_CLI] = runtime.cliPath;
17
+ if (env[OMO_LSP_DAEMON_VERSION] === undefined)
18
+ env[OMO_LSP_DAEMON_VERSION] = runtime.version;
19
+ return runtime.cliPath;
20
20
  }
21
21
  function resolveLspDaemonCli() {
22
22
  const packageCli = resolvePackageLspDaemonCliPath();
@@ -31,14 +31,20 @@ function resolvePackageLspDaemonCliPath() {
31
31
  try {
32
32
  return requireFromHere.resolve(PACKAGE_LSP_DAEMON_CLI);
33
33
  }
34
- catch {
34
+ catch (error) {
35
+ if (!(error instanceof Error))
36
+ throw error;
35
37
  return null;
36
38
  }
37
39
  }
38
40
  function resolveConfiguredLspDaemonCli(cliPath) {
41
+ const version = readDaemonPackageVersion(cliPath);
42
+ if (version === null) {
43
+ throw new Error(`Unable to determine packaged LSP daemon version beside ${cliPath}`);
44
+ }
39
45
  return {
40
46
  cliPath,
41
- version: readDaemonPackageVersion(cliPath),
47
+ version,
42
48
  };
43
49
  }
44
50
  function readDaemonPackageVersion(cliPath) {
@@ -48,7 +54,10 @@ function readDaemonPackageVersion(cliPath) {
48
54
  return parsed["version"];
49
55
  }
50
56
  }
51
- catch { }
57
+ catch (error) {
58
+ if (!(error instanceof Error))
59
+ throw error;
60
+ }
52
61
  return null;
53
62
  }
54
63
  function isRecord(value) {
@@ -1,12 +1,8 @@
1
- export interface DiagnosticsObservation {
2
- readonly filePath: string;
3
- readonly unavailable: boolean;
4
- }
1
+ import type { PostEditNotConfiguredCache } from "@oh-my-opencode/lsp-core/post-edit";
5
2
  export declare function sessionIdFrom(input: {
6
3
  readonly session_id?: unknown;
7
4
  }): string | undefined;
8
- export declare function shouldSkipUnavailableLspDiagnostics(filePath: string, sessionId: string | undefined): boolean;
9
- export declare function recordLspDiagnosticsObservations(sessionId: string | undefined, observations: readonly DiagnosticsObservation[]): void;
5
+ export declare function readLspPostEditCache(sessionId: string | undefined): PostEditNotConfiguredCache;
6
+ export declare function writeLspPostEditCache(sessionId: string | undefined, cache: PostEditNotConfiguredCache): void;
10
7
  export declare function markLspSessionCompacted(sessionId: string | undefined): void;
11
- export declare function isUnavailableLspDiagnostics(diagnostics: string): boolean;
12
8
  export declare function isLspDaemonUnreachableDiagnostics(diagnostics: string): boolean;
@@ -1,53 +1,26 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, extname, join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  export function sessionIdFrom(input) {
5
5
  return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
6
6
  }
7
- export function shouldSkipUnavailableLspDiagnostics(filePath, sessionId) {
7
+ export function readLspPostEditCache(sessionId) {
8
8
  if (sessionId === undefined)
9
- return false;
9
+ return { notConfiguredExtensions: new Set() };
10
10
  const state = readSessionState(sessionStatePath(sessionId));
11
- const extension = extensionKey(filePath);
12
- return (extension !== undefined &&
13
- state.postCompactProbePending !== true &&
14
- state.unavailableExtensions.includes(extension));
11
+ return { notConfiguredExtensions: new Set(state.notConfiguredExtensions) };
15
12
  }
16
- export function recordLspDiagnosticsObservations(sessionId, observations) {
17
- if (sessionId === undefined || observations.length === 0)
18
- return;
19
- const state = readSessionState(sessionStatePath(sessionId));
20
- const unavailableExtensions = new Set(state.unavailableExtensions);
21
- for (const observation of observations) {
22
- const extension = extensionKey(observation.filePath);
23
- if (extension === undefined)
24
- continue;
25
- if (observation.unavailable) {
26
- unavailableExtensions.add(extension);
27
- }
28
- else {
29
- unavailableExtensions.delete(extension);
30
- }
31
- }
32
- writeSessionState(sessionStatePath(sessionId), { unavailableExtensions: [...unavailableExtensions].sort() });
33
- }
34
- export function markLspSessionCompacted(sessionId) {
13
+ export function writeLspPostEditCache(sessionId, cache) {
35
14
  if (sessionId === undefined)
36
15
  return;
37
- const state = readSessionState(sessionStatePath(sessionId));
38
- if (state.unavailableExtensions.length === 0)
39
- return;
40
16
  writeSessionState(sessionStatePath(sessionId), {
41
- unavailableExtensions: state.unavailableExtensions,
42
- postCompactProbePending: true,
17
+ notConfiguredExtensions: [...cache.notConfiguredExtensions].sort(),
43
18
  });
44
19
  }
45
- export function isUnavailableLspDiagnostics(diagnostics) {
46
- const normalized = diagnostics.trim();
47
- return (normalized.includes("LSP request timeout (method: initialize)") ||
48
- normalized.includes("LSP server is still initializing") ||
49
- normalized.includes("NOT INSTALLED") ||
50
- normalized.includes("Command not found:"));
20
+ export function markLspSessionCompacted(sessionId) {
21
+ if (sessionId === undefined)
22
+ return;
23
+ writeSessionState(sessionStatePath(sessionId), emptyState());
51
24
  }
52
25
  export function isLspDaemonUnreachableDiagnostics(diagnostics) {
53
26
  return diagnostics.includes("LSP daemon unreachable");
@@ -59,8 +32,8 @@ function sessionStatePath(sessionId) {
59
32
  function readSessionState(path) {
60
33
  try {
61
34
  const parsed = JSON.parse(readFileSync(path, "utf8"));
62
- if (isLspSessionState(parsed))
63
- return parsed;
35
+ if (isRecord(parsed) && isLspSessionState(parsed))
36
+ return normalizeSessionState(parsed);
64
37
  return emptyState();
65
38
  }
66
39
  catch (error) {
@@ -74,21 +47,22 @@ function writeSessionState(path, state) {
74
47
  writeFileSync(path, `${JSON.stringify(state)}\n`);
75
48
  }
76
49
  function emptyState() {
77
- return { unavailableExtensions: [] };
78
- }
79
- function extensionKey(filePath) {
80
- const extension = extname(filePath).toLowerCase();
81
- return extension.length === 0 ? undefined : extension;
50
+ return { notConfiguredExtensions: [] };
82
51
  }
83
52
  function safePathSegment(value) {
84
53
  return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
85
54
  }
86
55
  function isLspSessionState(value) {
87
- if (!isRecord(value) || !Array.isArray(value["unavailableExtensions"]))
88
- return false;
89
- const postCompactProbePending = value["postCompactProbePending"];
90
- return (value["unavailableExtensions"].every((item) => typeof item === "string") &&
91
- (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean"));
56
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
57
+ return Array.isArray(notConfiguredExtensions) && notConfiguredExtensions.every((item) => typeof item === "string");
58
+ }
59
+ function normalizeSessionState(value) {
60
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
61
+ return {
62
+ notConfiguredExtensions: Array.isArray(notConfiguredExtensions)
63
+ ? notConfiguredExtensions.filter((item) => typeof item === "string").sort()
64
+ : [],
65
+ };
92
66
  }
93
67
  function isRecord(value) {
94
68
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 60,
11
- "statusMessage": "(OmO 4.18.0) Checking LSP Diagnostics"
11
+ "statusMessage": "(OmO 4.18.2) Checking LSP Diagnostics"
12
12
  }
13
13
  ]
14
14
  }
@@ -21,7 +21,7 @@
21
21
  "type": "command",
22
22
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
23
23
  "timeout": 5,
24
- "statusMessage": "(OmO 4.18.0) Resetting LSP Diagnostics Cache"
24
+ "statusMessage": "(OmO 4.18.2) Resetting LSP Diagnostics Cache"
25
25
  }
26
26
  ]
27
27
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-lsp",
3
- "version": "4.18.0",
3
+ "version": "4.18.2",
4
4
  "description": "Codex plugin that exposes Language Server Protocol tools and post-edit diagnostics.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "bootstrap": "node scripts/build-lsp-tools.mjs && node scripts/build-lsp-daemon.mjs",
40
40
  "prebuild": "node scripts/build-lsp-tools.mjs && node scripts/build-lsp-daemon.mjs",
41
- "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
41
+ "build": "node scripts/build-runtime.mjs",
42
42
  "pretest": "npm run build --silent",
43
43
  "test": "node scripts/test.mjs",
44
44
  "test:watch": "vitest",
@@ -50,6 +50,7 @@
50
50
  "check": "tsc --noEmit && biome check src test && tsc -p tsconfig.build.json"
51
51
  },
52
52
  "dependencies": {
53
+ "@oh-my-opencode/lsp-core": "file:../../../../lsp-core",
53
54
  "@code-yeongyu/lsp-daemon": "file:../../../../lsp-daemon"
54
55
  },
55
56
  "devDependencies": {
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from "node:child_process";
3
- import { existsSync, statSync } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, statSync, symlinkSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const here = dirname(fileURLToPath(import.meta.url));
8
+ const componentRoot = join(here, "..");
8
9
  const force = process.argv.includes("--force");
9
10
 
10
11
  // Ordered candidate list:
@@ -18,6 +19,8 @@ const candidates = [
18
19
  function requiredOutputs(dir) {
19
20
  return [
20
21
  join(dir, "dist", "cli.js"),
22
+ join(dir, "dist", "client.js"),
23
+ join(dir, "dist", "client.d.ts"),
21
24
  join(dir, "dist", "index.js"),
22
25
  join(dir, "dist", "index.d.ts"),
23
26
  ];
@@ -30,12 +33,14 @@ for (const dir of candidates) {
30
33
  if (existsSync(packageJson)) {
31
34
  const outputs = requiredOutputs(dir);
32
35
  if (!force && isBuildFresh(packageJson, outputs)) {
36
+ ensureComponentPackageLink(dir);
33
37
  process.exit(0);
34
38
  }
35
39
  console.log("Installing repository lsp-daemon dependencies...");
36
40
  execSync("npm ci", { cwd: dir, stdio: "inherit" });
37
41
  console.log("Building repository lsp-daemon...");
38
42
  execSync("npm run build", { cwd: dir, stdio: "inherit" });
43
+ ensureComponentPackageLink(dir);
39
44
  console.log("Done.");
40
45
  process.exit(0);
41
46
  }
@@ -46,6 +51,7 @@ for (const dir of candidates) {
46
51
  for (const dir of candidates) {
47
52
  const outputs = requiredOutputs(dir);
48
53
  if (outputs.every((p) => existsSync(p))) {
54
+ ensureComponentPackageLink(dir);
49
55
  console.log(`Using bundled lsp-daemon dist at ${dir}.`);
50
56
  process.exit(0);
51
57
  }
@@ -66,3 +72,27 @@ function isBuildFresh(inputPath, outputPaths) {
66
72
  const inputMtime = statSync(inputPath).mtimeMs;
67
73
  return outputPaths.every((path) => statSync(path).mtimeMs >= inputMtime);
68
74
  }
75
+
76
+ function ensureComponentPackageLink(packageDir) {
77
+ const scopeDir = join(componentRoot, "node_modules", "@code-yeongyu");
78
+ const linkPath = join(scopeDir, "lsp-daemon");
79
+ mkdirSync(scopeDir, { recursive: true });
80
+ if (isSymlinkToUsablePackage(linkPath, packageDir)) return;
81
+ rmSync(linkPath, { recursive: true, force: true });
82
+ symlinkSync(packageDir, linkPath, "junction");
83
+ }
84
+
85
+ function isSymlinkToUsablePackage(linkPath, packageDir) {
86
+ try {
87
+ const stat = lstatSync(linkPath);
88
+ return (
89
+ stat.isSymbolicLink() &&
90
+ existsSync(packageDir) &&
91
+ realpathSync(linkPath) === realpathSync(packageDir) &&
92
+ requiredOutputs(linkPath).every((path) => existsSync(path))
93
+ );
94
+ } catch (error) {
95
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
96
+ throw error;
97
+ }
98
+ }
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { delimiter, join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+
8
+ async function makeFixture() {
9
+ const root = await mkdtemp(join(tmpdir(), "codex-lsp-daemon-build-"));
10
+ const componentRoot = join(root, "packages", "omo-codex", "plugin", "components", "lsp");
11
+ const packageDir = join(root, "packages", "lsp-daemon");
12
+ const script = join(componentRoot, "scripts", "build-lsp-daemon.mjs");
13
+ await mkdir(join(componentRoot, "scripts"), { recursive: true });
14
+ await mkdir(join(packageDir, "dist"), { recursive: true });
15
+ await copyFile(new URL("./build-lsp-daemon.mjs", import.meta.url), script);
16
+ await writeFile(join(packageDir, "package.json"), "{}\n");
17
+ await writeDaemonOutputs(packageDir);
18
+ return { root, componentRoot, packageDir, script };
19
+ }
20
+
21
+ async function writeDaemonOutputs(packageDir) {
22
+ for (const output of ["cli.js", "client.js", "client.d.ts", "index.js", "index.d.ts"]) {
23
+ await writeFile(join(packageDir, "dist", output), `${output}\n`);
24
+ }
25
+ }
26
+
27
+ function runScript(script, fakeBin, args = []) {
28
+ return spawnSync(process.execPath, [script, ...args], {
29
+ encoding: "utf8",
30
+ env: { ...process.env, PATH: fakeBin ? `${fakeBin}${delimiter}${process.env.PATH ?? ""}` : process.env.PATH },
31
+ });
32
+ }
33
+
34
+ test("#given fresh daemon dist #when bootstrapping component #then local package link is created", async () => {
35
+ // given
36
+ const fixture = await makeFixture();
37
+ const link = join(fixture.componentRoot, "node_modules", "@code-yeongyu", "lsp-daemon");
38
+
39
+ // when
40
+ const result = runScript(fixture.script);
41
+
42
+ // then
43
+ assert.equal(result.status, 0, result.stderr);
44
+ assert.equal((await lstat(link)).isSymbolicLink(), true);
45
+ assert.equal(await realpath(link), await realpath(fixture.packageDir));
46
+ });
47
+
48
+ test("#given client outputs are missing #when bootstrapping component #then daemon rebuild runs before linking", async () => {
49
+ // given
50
+ const fixture = await makeFixture();
51
+ await rm(join(fixture.packageDir, "dist", "client.d.ts"));
52
+ const fakeBin = join(fixture.root, "bin");
53
+ const npmLog = join(fixture.root, "npm.log");
54
+ await mkdir(fakeBin, { recursive: true });
55
+ await writeFile(
56
+ join(fakeBin, "npm.js"),
57
+ `const { appendFileSync, mkdirSync, writeFileSync } = require("node:fs");\n` +
58
+ `const { join } = require("node:path");\n` +
59
+ `appendFileSync(${JSON.stringify(npmLog)}, process.argv.slice(2).join(" ") + "\\n");\n` +
60
+ `if (process.argv.slice(2).join(" ") === "run build") {\n` +
61
+ ` const dist = join(process.cwd(), "dist");\n` +
62
+ ` mkdirSync(dist, { recursive: true });\n` +
63
+ ` for (const name of ["cli.js", "client.js", "client.d.ts", "index.js", "index.d.ts"]) writeFileSync(join(dist, name), name + "\\n");\n` +
64
+ `}\n`,
65
+ );
66
+ await writeFile(join(fakeBin, "npm"), "#!/usr/bin/env node\nrequire('./npm.js');\n");
67
+ await chmod(join(fakeBin, "npm"), 0o755);
68
+
69
+ // when
70
+ const result = runScript(fixture.script, fakeBin);
71
+
72
+ // then
73
+ assert.equal(result.status, 0, result.stderr);
74
+ assert.match(await readFile(npmLog, "utf8"), /ci\nrun build\n/u);
75
+ assert.equal(await realpath(join(fixture.componentRoot, "node_modules", "@code-yeongyu", "lsp-daemon")), await realpath(fixture.packageDir));
76
+ });