oh-my-opencode 4.9.2 → 4.10.0

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 (211) hide show
  1. package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
  2. package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
  3. package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  4. package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  5. package/bin/platform.js +5 -0
  6. package/bin/platform.test.ts +56 -0
  7. package/dist/agents/atlas/agent.d.ts +4 -3
  8. package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
  9. package/dist/agents/hephaestus/agent.d.ts +5 -0
  10. package/dist/agents/hephaestus/index.d.ts +1 -1
  11. package/dist/agents/metis.d.ts +1 -0
  12. package/dist/agents/prometheus/system-prompt.d.ts +1 -1
  13. package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
  14. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  15. package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
  16. package/dist/agents/types.d.ts +2 -2
  17. package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
  18. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
  19. package/dist/cli/doctor/constants.d.ts +1 -1
  20. package/dist/cli/index.js +929 -291
  21. package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
  22. package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
  23. package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
  24. package/dist/cli/model-fallback.d.ts +1 -0
  25. package/dist/cli/provider-availability.d.ts +2 -0
  26. package/dist/cli-node/index.js +929 -291
  27. package/dist/config/schema/agent-overrides.d.ts +80 -16
  28. package/dist/config/schema/experimental.d.ts +0 -1
  29. package/dist/config/schema/hooks.d.ts +0 -1
  30. package/dist/config/schema/internal/permission.d.ts +5 -1
  31. package/dist/config/schema/oh-my-opencode-config.d.ts +75 -16
  32. package/dist/create-hooks.d.ts +0 -1
  33. package/dist/features/background-agent/index.d.ts +1 -1
  34. package/dist/features/background-agent/manager.d.ts +6 -0
  35. package/dist/features/background-agent/types.d.ts +2 -0
  36. package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
  37. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  38. package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
  39. package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
  40. package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
  41. package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
  42. package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
  43. package/dist/features/tool-metadata-store/store.d.ts +5 -0
  44. package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
  45. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
  46. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
  47. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
  48. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
  49. package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
  52. package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
  53. package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
  56. package/dist/index.js +2991 -2367
  57. package/dist/oh-my-opencode.schema.json +120 -18
  58. package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
  59. package/dist/plugin/event-session-lifecycle.d.ts +0 -3
  60. package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
  61. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  62. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  63. package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
  64. package/dist/shared/plugin-identity.d.ts +2 -2
  65. package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
  66. package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
  67. package/dist/shared/tmux/tmux-utils.d.ts +1 -0
  68. package/dist/tools/background-task/clients.d.ts +2 -0
  69. package/dist/tools/background-task/full-session-format.d.ts +1 -0
  70. package/dist/tools/background-task/types.d.ts +1 -0
  71. package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
  72. package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
  73. package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
  74. package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
  75. package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
  76. package/dist/tools/look-at/types.d.ts +2 -0
  77. package/dist/tools/session-manager/types.d.ts +1 -0
  78. package/dist/tools/skill-mcp/types.d.ts +1 -0
  79. package/package.json +14 -13
  80. package/packages/ast-grep-mcp/dist/cli.js +50 -17
  81. package/packages/lsp-daemon/dist/cli.js +8 -5
  82. package/packages/lsp-daemon/dist/index.js +8 -5
  83. package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
  84. package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
  85. package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
  86. package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
  87. package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
  88. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  89. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
  90. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
  91. package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
  92. package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
  93. package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
  94. package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
  95. package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
  96. package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
  97. package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
  98. package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
  99. package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
  100. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
  101. package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
  102. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
  103. package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
  104. package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
  105. package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
  106. package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
  107. package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
  108. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  109. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  110. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  111. package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
  112. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
  113. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  114. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  115. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
  116. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
  117. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
  118. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
  119. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  120. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  121. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
  122. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  123. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +4 -4
  124. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  125. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  126. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
  127. package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
  128. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  129. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  130. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
  131. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
  132. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
  133. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
  134. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
  135. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
  136. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
  138. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  139. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  140. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
  141. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
  142. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
  143. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
  144. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
  145. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
  146. package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
  147. package/packages/omo-codex/plugin/package-lock.json +193 -193
  148. package/packages/omo-codex/plugin/package.json +1 -1
  149. package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
  150. package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
  151. package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
  152. package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
  153. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  154. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  155. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
  156. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  157. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
  158. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
  159. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
  160. package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
  161. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
  162. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
  163. package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
  164. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
  165. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
  166. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
  167. package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
  168. package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
  169. package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
  170. package/packages/omo-codex/scripts/install/agents.mjs +78 -5
  171. package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
  172. package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
  173. package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
  174. package/packages/omo-codex/scripts/install/config.d.mts +35 -0
  175. package/packages/omo-codex/scripts/install/config.mjs +13 -3
  176. package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
  177. package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
  178. package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
  179. package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
  180. package/packages/omo-codex/scripts/install-local.mjs +3 -2
  181. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  182. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  183. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
  184. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  185. package/dist/hooks/session-recovery/constants.d.ts +0 -4
  186. package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
  187. package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
  188. package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
  189. package/dist/hooks/session-recovery/hook.d.ts +0 -4
  190. package/dist/hooks/session-recovery/index.d.ts +0 -5
  191. package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
  192. package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
  193. package/dist/hooks/session-recovery/message-state.d.ts +0 -4
  194. package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
  195. package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
  196. package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
  197. package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
  198. package/dist/hooks/session-recovery/resume.d.ts +0 -7
  199. package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
  200. package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
  201. package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
  202. package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
  203. package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
  204. package/dist/hooks/session-recovery/storage.d.ts +0 -20
  205. package/dist/plugin/event-session-recovery.d.ts +0 -9
  206. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
  207. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
  208. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
  209. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
  210. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
  211. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
@@ -0,0 +1,464 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { deflateRawSync } from "node:zlib";
8
+
9
+ import type { FetchLike } from "../src/download.ts";
10
+ import {
11
+ runSgProvision,
12
+ SG_FORCE_PROVISION_ENV_KEY,
13
+ SG_PROVISION_COMPONENT,
14
+ sgProvisionDestination,
15
+ } from "../src/provision.ts";
16
+ import {
17
+ BOOTSTRAP_DOCTOR_HINT,
18
+ defaultWorkerSteps,
19
+ readBootstrapState,
20
+ runBootstrapWorker,
21
+ type BootstrapWorkerContext,
22
+ } from "../src/worker.ts";
23
+
24
+ const temporaryDirectories: string[] = [];
25
+
26
+ function createTemporaryDirectory(prefix: string): string {
27
+ const directory = mkdtempSync(join(tmpdir(), prefix));
28
+ temporaryDirectories.push(directory);
29
+ return directory;
30
+ }
31
+
32
+ afterEach(() => {
33
+ for (const directory of temporaryDirectories.splice(0)) {
34
+ rmSync(directory, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ function sha256Hex(bytes: Uint8Array): string {
39
+ return createHash("sha256").update(bytes).digest("hex");
40
+ }
41
+
42
+ const CRC_TABLE = ((): Uint32Array => {
43
+ const table = new Uint32Array(256);
44
+ for (let index = 0; index < 256; index += 1) {
45
+ let value = index;
46
+ for (let bit = 0; bit < 8; bit += 1) {
47
+ value = (value & 1) === 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
48
+ }
49
+ table[index] = value >>> 0;
50
+ }
51
+ return table;
52
+ })();
53
+
54
+ function crc32(bytes: Uint8Array): number {
55
+ let crc = 0xffffffff;
56
+ for (const byte of bytes) {
57
+ crc = (CRC_TABLE[(crc ^ byte) & 0xff] as number) ^ (crc >>> 8);
58
+ }
59
+ return (crc ^ 0xffffffff) >>> 0;
60
+ }
61
+
62
+ /** Builds a real (deflate-compressed) zip archive, mirroring the ast-grep release asset layout. */
63
+ function makeZip(entries: ReadonlyArray<{ name: string; data: Uint8Array }>): Buffer {
64
+ const locals: Buffer[] = [];
65
+ const centrals: Buffer[] = [];
66
+ let localOffset = 0;
67
+ for (const entry of entries) {
68
+ const name = Buffer.from(entry.name, "utf8");
69
+ const compressed = deflateRawSync(entry.data);
70
+ const checksum = crc32(entry.data);
71
+ const local = Buffer.alloc(30 + name.length + compressed.length);
72
+ local.writeUInt32LE(0x04034b50, 0);
73
+ local.writeUInt16LE(20, 4);
74
+ local.writeUInt16LE(8, 8);
75
+ local.writeUInt32LE(checksum, 14);
76
+ local.writeUInt32LE(compressed.length, 18);
77
+ local.writeUInt32LE(entry.data.length, 22);
78
+ local.writeUInt16LE(name.length, 26);
79
+ name.copy(local, 30);
80
+ compressed.copy(local, 30 + name.length);
81
+ locals.push(local);
82
+
83
+ const central = Buffer.alloc(46 + name.length);
84
+ central.writeUInt32LE(0x02014b50, 0);
85
+ central.writeUInt16LE(0x031e, 4);
86
+ central.writeUInt16LE(20, 6);
87
+ central.writeUInt16LE(8, 10);
88
+ central.writeUInt32LE(checksum, 16);
89
+ central.writeUInt32LE(compressed.length, 20);
90
+ central.writeUInt32LE(entry.data.length, 24);
91
+ central.writeUInt16LE(name.length, 28);
92
+ central.writeUInt32LE((0o100755 << 16) >>> 0, 38);
93
+ central.writeUInt32LE(localOffset, 42);
94
+ name.copy(central, 46);
95
+ centrals.push(central);
96
+ localOffset += local.length;
97
+ }
98
+ const centralDirectory = Buffer.concat(centrals);
99
+ const endRecord = Buffer.alloc(22);
100
+ endRecord.writeUInt32LE(0x06054b50, 0);
101
+ endRecord.writeUInt16LE(entries.length, 8);
102
+ endRecord.writeUInt16LE(entries.length, 10);
103
+ endRecord.writeUInt32LE(centralDirectory.length, 12);
104
+ endRecord.writeUInt32LE(localOffset, 16);
105
+ return Buffer.concat([...locals, centralDirectory, endRecord]);
106
+ }
107
+
108
+ // Real release zips ship a tiny PATH-searching `sg` alias shim plus the
109
+ // standalone `ast-grep` binary; provisioning must install the standalone one.
110
+ const STANDALONE_BYTES = new Uint8Array(Buffer.alloc(16_384, "standalone-ast-grep-binary "));
111
+ const SHIM_BYTES = new Uint8Array(Buffer.from("path-searching-sg-shim"));
112
+ const FIXTURE_ZIP = makeZip([
113
+ { data: SHIM_BYTES, name: "sg" },
114
+ { data: STANDALONE_BYTES, name: "ast-grep" },
115
+ ]);
116
+ const FIXTURE_VERSION = "9.9.9";
117
+
118
+ async function writeAstGrepManifest(
119
+ manifestsDir: string,
120
+ options: { sha256?: string; platforms?: Record<string, { url: string; sha256: string }> } = {},
121
+ ): Promise<void> {
122
+ await mkdir(manifestsDir, { recursive: true });
123
+ const platforms = options.platforms ?? {
124
+ "linux-x64": {
125
+ sha256: options.sha256 ?? sha256Hex(FIXTURE_ZIP),
126
+ url: "https://example.invalid/releases/9.9.9/app-x86_64-unknown-linux-gnu.zip",
127
+ },
128
+ };
129
+ const manifest = { name: "ast-grep", platforms, version: FIXTURE_VERSION };
130
+ await writeFile(join(manifestsDir, "ast-grep.json"), JSON.stringify(manifest, null, "\t"), "utf8");
131
+ }
132
+
133
+ function fetchReturning(bytes: Uint8Array): { fetchImpl: FetchLike; calls: string[] } {
134
+ const calls: string[] = [];
135
+ const fetchImpl: FetchLike = async (url) => {
136
+ calls.push(url);
137
+ return new Response(bytes, { status: 200 });
138
+ };
139
+ return { calls, fetchImpl };
140
+ }
141
+
142
+ function makeContext(options: {
143
+ codexHome: string;
144
+ manifestDir: string;
145
+ pluginData: string;
146
+ env?: Record<string, string | undefined>;
147
+ }): BootstrapWorkerContext {
148
+ return {
149
+ codexHome: options.codexHome,
150
+ env: options.env ?? {},
151
+ flags: { manifestDir: options.manifestDir, once: false, only: "sg" },
152
+ now: 1_700_000_000_000,
153
+ platform: "linux",
154
+ pluginData: options.pluginData,
155
+ pluginRoot: join(options.pluginData, "plugin-root"),
156
+ pluginVersion: "1.0.0",
157
+ };
158
+ }
159
+
160
+ async function listFilesRecursively(root: string): Promise<string[]> {
161
+ try {
162
+ const entries = await readdir(root, { recursive: true, withFileTypes: true });
163
+ return entries.filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name));
164
+ } catch {
165
+ return [];
166
+ }
167
+ }
168
+
169
+ async function readBootstrapLog(pluginData: string): Promise<string> {
170
+ try {
171
+ return await readFile(join(pluginData, "bootstrap", "bootstrap.log"), "utf8");
172
+ } catch {
173
+ return "";
174
+ }
175
+ }
176
+
177
+ describe("runSgProvision", () => {
178
+ it("#given resolution misses #when provisioning from a pinned zip #then the standalone binary lands at the resolver probe path with mode 755 and no staging leftovers", async () => {
179
+ // given
180
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
181
+ const codexHome = join(root, "codex-home");
182
+ const manifestDir = join(root, "manifests");
183
+ await writeAstGrepManifest(manifestDir);
184
+ const { calls, fetchImpl } = fetchReturning(FIXTURE_ZIP);
185
+ const probed: string[] = [];
186
+ const context = makeContext({ codexHome, manifestDir, pluginData: join(root, "data") });
187
+
188
+ // when
189
+ const outcome = await runSgProvision(context, {
190
+ arch: "x64",
191
+ fetchImpl,
192
+ resolvePreexistingSg: () => null,
193
+ runVersionProbe: async (binaryPath: string) => {
194
+ probed.push(binaryPath);
195
+ return `ast-grep ${FIXTURE_VERSION}\n`;
196
+ },
197
+ });
198
+
199
+ // then
200
+ const destination = join(codexHome, "runtime", "ast-grep", "linux-x64", "sg");
201
+ expect(sgProvisionDestination(context, "x64")).toBe(destination);
202
+ expect(outcome.degraded).toEqual([]);
203
+ expect(calls).toEqual(["https://example.invalid/releases/9.9.9/app-x86_64-unknown-linux-gnu.zip"]);
204
+ expect(probed).toEqual([destination]);
205
+ expect(new Uint8Array(await readFile(destination))).toEqual(STANDALONE_BYTES);
206
+ expect((await stat(destination)).mode & 0o777).toBe(0o755);
207
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([destination]);
208
+ expect(await readBootstrapLog(context.pluginData)).toContain(`"sg":"provisioned:${destination}"`);
209
+ });
210
+
211
+ it("#given a preexisting sg resolution #when provisioning #then it short-circuits without fetching and records a preexisting note in the log", async () => {
212
+ // given
213
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
214
+ const codexHome = join(root, "codex-home");
215
+ const manifestDir = join(root, "manifests");
216
+ await writeAstGrepManifest(manifestDir);
217
+ const { calls, fetchImpl } = fetchReturning(FIXTURE_ZIP);
218
+ const context = makeContext({ codexHome, manifestDir, pluginData: join(root, "data") });
219
+
220
+ // when
221
+ const outcome = await runSgProvision(context, {
222
+ arch: "x64",
223
+ fetchImpl,
224
+ resolvePreexistingSg: () => "/opt/homebrew/bin/sg",
225
+ runVersionProbe: async () => {
226
+ throw new Error("must not probe on short-circuit");
227
+ },
228
+ });
229
+
230
+ // then
231
+ expect(outcome.degraded).toEqual([]);
232
+ expect(calls).toEqual([]);
233
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([]);
234
+ expect(await readBootstrapLog(context.pluginData)).toContain('"sg":"preexisting:/opt/homebrew/bin/sg"');
235
+ });
236
+
237
+ it("#given OMO_BOOTSTRAP_FORCE_PROVISION=1 #when a preexisting sg would resolve #then resolution is bypassed and provisioning still runs", async () => {
238
+ // given
239
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
240
+ const codexHome = join(root, "codex-home");
241
+ const manifestDir = join(root, "manifests");
242
+ await writeAstGrepManifest(manifestDir);
243
+ const { calls, fetchImpl } = fetchReturning(FIXTURE_ZIP);
244
+ const resolverCalls: string[] = [];
245
+ const context = makeContext({
246
+ codexHome,
247
+ env: { [SG_FORCE_PROVISION_ENV_KEY]: "1" },
248
+ manifestDir,
249
+ pluginData: join(root, "data"),
250
+ });
251
+
252
+ // when
253
+ const outcome = await runSgProvision(context, {
254
+ arch: "x64",
255
+ fetchImpl,
256
+ resolvePreexistingSg: () => {
257
+ resolverCalls.push("resolved");
258
+ return "/opt/homebrew/bin/sg";
259
+ },
260
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
261
+ });
262
+
263
+ // then
264
+ expect(outcome.degraded).toEqual([]);
265
+ expect(resolverCalls).toEqual([]);
266
+ expect(calls).toHaveLength(1);
267
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([
268
+ join(codexHome, "runtime", "ast-grep", "linux-x64", "sg"),
269
+ ]);
270
+ });
271
+
272
+ it("#given a tampered checksum #when provisioning #then a degraded ast_grep entry names the mismatch and no file is left under runtime", async () => {
273
+ // given
274
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
275
+ const codexHome = join(root, "codex-home");
276
+ const manifestDir = join(root, "manifests");
277
+ await writeAstGrepManifest(manifestDir, { sha256: sha256Hex(new TextEncoder().encode("tampered")) });
278
+ const context = makeContext({ codexHome, manifestDir, pluginData: join(root, "data") });
279
+
280
+ // when
281
+ const outcome = await runSgProvision(context, {
282
+ arch: "x64",
283
+ fetchImpl: fetchReturning(FIXTURE_ZIP).fetchImpl,
284
+ resolvePreexistingSg: () => null,
285
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
286
+ });
287
+
288
+ // then
289
+ expect(outcome.degraded).toHaveLength(1);
290
+ const entry = outcome.degraded[0];
291
+ expect(entry?.component).toBe(SG_PROVISION_COMPONENT);
292
+ expect(entry?.reason).toMatch(/checksum mismatch/i);
293
+ expect(entry?.hint).toBe(BOOTSTRAP_DOCTOR_HINT);
294
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([]);
295
+ });
296
+
297
+ it("#given a binary that reports the wrong version #when provisioning #then the provisioned file is removed and a degraded entry names both versions", async () => {
298
+ // given
299
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
300
+ const codexHome = join(root, "codex-home");
301
+ const manifestDir = join(root, "manifests");
302
+ await writeAstGrepManifest(manifestDir);
303
+ const context = makeContext({ codexHome, manifestDir, pluginData: join(root, "data") });
304
+
305
+ // when
306
+ const outcome = await runSgProvision(context, {
307
+ arch: "x64",
308
+ fetchImpl: fetchReturning(FIXTURE_ZIP).fetchImpl,
309
+ resolvePreexistingSg: () => null,
310
+ runVersionProbe: async () => "ast-grep 0.0.1",
311
+ });
312
+
313
+ // then
314
+ expect(outcome.degraded).toHaveLength(1);
315
+ const entry = outcome.degraded[0];
316
+ expect(entry?.component).toBe(SG_PROVISION_COMPONENT);
317
+ expect(entry?.reason).toContain("0.0.1");
318
+ expect(entry?.reason).toContain(FIXTURE_VERSION);
319
+ expect(entry?.hint).toBe(BOOTSTRAP_DOCTOR_HINT);
320
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([]);
321
+ });
322
+
323
+ it("#given a platform absent from the manifest #when provisioning #then a degraded entry names the unsupported platform without fetching", async () => {
324
+ // given
325
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
326
+ const codexHome = join(root, "codex-home");
327
+ const manifestDir = join(root, "manifests");
328
+ await writeAstGrepManifest(manifestDir);
329
+ const { calls, fetchImpl } = fetchReturning(FIXTURE_ZIP);
330
+ const context = makeContext({ codexHome, manifestDir, pluginData: join(root, "data") });
331
+
332
+ // when
333
+ const outcome = await runSgProvision(context, {
334
+ arch: "riscv64",
335
+ fetchImpl,
336
+ resolvePreexistingSg: () => null,
337
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
338
+ });
339
+
340
+ // then
341
+ expect(outcome.degraded).toHaveLength(1);
342
+ const entry = outcome.degraded[0];
343
+ expect(entry?.component).toBe(SG_PROVISION_COMPONENT);
344
+ expect(entry?.reason).toMatch(/unsupported platform/i);
345
+ expect(entry?.reason).toContain("linux-riscv64");
346
+ expect(calls).toEqual([]);
347
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([]);
348
+ });
349
+
350
+ it("#given win32 #when provisioning #then the destination binary is sg.exe extracted from the .exe entry", async () => {
351
+ // given
352
+ const root = createTemporaryDirectory("omo-bootstrap-provision-");
353
+ const codexHome = join(root, "codex-home");
354
+ const manifestDir = join(root, "manifests");
355
+ const windowsZip = makeZip([
356
+ { data: SHIM_BYTES, name: "sg.exe" },
357
+ { data: STANDALONE_BYTES, name: "ast-grep.exe" },
358
+ ]);
359
+ await writeAstGrepManifest(manifestDir, {
360
+ platforms: {
361
+ "win32-x64": {
362
+ sha256: sha256Hex(windowsZip),
363
+ url: "https://example.invalid/releases/9.9.9/app-x86_64-pc-windows-msvc.zip",
364
+ },
365
+ },
366
+ });
367
+ const context: BootstrapWorkerContext = {
368
+ ...makeContext({ codexHome, manifestDir, pluginData: join(root, "data") }),
369
+ platform: "win32",
370
+ };
371
+
372
+ // when
373
+ const outcome = await runSgProvision(context, {
374
+ arch: "x64",
375
+ fetchImpl: fetchReturning(windowsZip).fetchImpl,
376
+ resolvePreexistingSg: () => null,
377
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
378
+ });
379
+
380
+ // then
381
+ const destination = join(codexHome, "runtime", "ast-grep", "win32-x64", "sg.exe");
382
+ expect(outcome.degraded).toEqual([]);
383
+ expect(new Uint8Array(await readFile(destination))).toEqual(STANDALONE_BYTES);
384
+ });
385
+ });
386
+
387
+ describe("worker sg step wiring", () => {
388
+ it("#given --only sg with injected seams #when the worker runs #then the default sg step provisions through the manifest-dir flag and writes a success state", async () => {
389
+ // given
390
+ const root = createTemporaryDirectory("omo-bootstrap-provision-worker-");
391
+ const codexHome = join(root, "codex-home");
392
+ const manifestDir = join(root, "manifests");
393
+ const pluginData = join(root, "data");
394
+ const pluginRoot = join(root, "plugin-root");
395
+ await writeAstGrepManifest(manifestDir);
396
+ await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
397
+ await writeFile(join(pluginRoot, ".codex-plugin", "plugin.json"), JSON.stringify({ version: "1.2.3" }), "utf8");
398
+ const { calls, fetchImpl } = fetchReturning(FIXTURE_ZIP);
399
+
400
+ // when
401
+ const result = await runBootstrapWorker({
402
+ argv: ["--only", "sg", "--codex-home", codexHome, "--manifest-dir", manifestDir],
403
+ env: { PLUGIN_DATA: pluginData, PLUGIN_ROOT: pluginRoot },
404
+ platform: "linux",
405
+ steps: defaultWorkerSteps({
406
+ sg: {
407
+ arch: "x64",
408
+ fetchImpl,
409
+ resolvePreexistingSg: () => null,
410
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
411
+ },
412
+ }),
413
+ });
414
+
415
+ // then
416
+ expect(result.ran).toBe(true);
417
+ if (!result.ran) throw new Error("expected the worker to run");
418
+ expect(result.status).toBe("success");
419
+ expect(calls).toHaveLength(1);
420
+ const state = await readBootstrapState(join(pluginData, "bootstrap", "state.json"));
421
+ expect(state.lastStatus).toBe("success");
422
+ expect(state.degraded).toEqual([]);
423
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([
424
+ join(codexHome, "runtime", "ast-grep", "linux-x64", "sg"),
425
+ ]);
426
+ });
427
+
428
+ it("#given --only sg with a tampered manifest #when the worker runs #then it finishes degraded with an ast_grep checksum entry and exit-0 semantics", async () => {
429
+ // given
430
+ const root = createTemporaryDirectory("omo-bootstrap-provision-worker-");
431
+ const codexHome = join(root, "codex-home");
432
+ const manifestDir = join(root, "manifests");
433
+ const pluginData = join(root, "data");
434
+ const pluginRoot = join(root, "plugin-root");
435
+ await writeAstGrepManifest(manifestDir, { sha256: sha256Hex(new TextEncoder().encode("tampered")) });
436
+ await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
437
+ await writeFile(join(pluginRoot, ".codex-plugin", "plugin.json"), JSON.stringify({ version: "1.2.3" }), "utf8");
438
+
439
+ // when
440
+ const result = await runBootstrapWorker({
441
+ argv: ["--only", "sg", "--codex-home", codexHome, "--manifest-dir", manifestDir],
442
+ env: { PLUGIN_DATA: pluginData, PLUGIN_ROOT: pluginRoot },
443
+ platform: "linux",
444
+ steps: defaultWorkerSteps({
445
+ sg: {
446
+ arch: "x64",
447
+ fetchImpl: fetchReturning(FIXTURE_ZIP).fetchImpl,
448
+ resolvePreexistingSg: () => null,
449
+ runVersionProbe: async () => `ast-grep ${FIXTURE_VERSION}`,
450
+ },
451
+ }),
452
+ });
453
+
454
+ // then
455
+ expect(result.ran).toBe(true);
456
+ if (!result.ran) throw new Error("expected the worker to run");
457
+ expect(result.status).toBe("degraded");
458
+ const state = await readBootstrapState(join(pluginData, "bootstrap", "state.json"));
459
+ expect(state.lastStatus).toBe("degraded");
460
+ expect(state.degraded?.[0]?.component).toBe(SG_PROVISION_COMPONENT);
461
+ expect(state.degraded?.[0]?.reason).toMatch(/checksum mismatch/i);
462
+ expect(await listFilesRecursively(join(codexHome, "runtime"))).toEqual([]);
463
+ });
464
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noPropertyAccessFromIndexSignature": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noImplicitOverride": true,
13
+ "noImplicitReturns": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "esModuleInterop": true,
18
+ "allowImportingTsExtensions": true,
19
+ "skipLibCheck": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "types": ["node", "bun-types"],
22
+ "noEmit": true
23
+ },
24
+ "include": ["src/**/*", "test/**/*"]
25
+ }
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 30,
11
- "statusMessage": "LazyCodex(4.9.2): Checking Comments"
11
+ "statusMessage": "LazyCodex(4.10.0): Checking Comments"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-comment-checker",
3
- "version": "4.9.2",
3
+ "version": "4.10.0",
4
4
  "description": "Codex plugin that runs comment-checker after edit-like PostToolUse hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -46,10 +46,10 @@
46
46
  "@code-yeongyu/comment-checker": "^0.8.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@biomejs/biome": "2.4.15",
50
- "@types/node": "^25.7.0",
49
+ "@biomejs/biome": "2.4.16",
50
+ "@types/node": "^25.9.3",
51
51
  "typescript": "^6.0.3",
52
- "vitest": "^4.1.5"
52
+ "vitest": "^4.1.8"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=20.0.0"
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
10
10
  "timeout": 5,
11
- "statusMessage": "LazyCodex(4.9.2): Recommending Git Bash Mcp"
11
+ "statusMessage": "LazyCodex(4.10.0): Recommending Git Bash Mcp"
12
12
  }
13
13
  ]
14
14
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
22
22
  "timeout": 5,
23
- "statusMessage": "LazyCodex(4.9.2): Resetting Git Bash Mcp Reminder"
23
+ "statusMessage": "LazyCodex(4.10.0): Resetting Git Bash Mcp Reminder"
24
24
  }
25
25
  ]
26
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-git-bash-hook",
3
- "version": "4.9.2",
3
+ "version": "4.10.0",
4
4
  "description": "Codex hook component that reminds Windows sessions to prefer the OMO git_bash MCP.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -17,7 +17,7 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "^25.7.0",
20
+ "@types/node": "^25.9.3",
21
21
  "typescript": "^6.0.3"
22
22
  },
23
23
  "engines": {
@@ -12,22 +12,18 @@ async function runHookCli(runHook, stdin) {
12
12
  const raw = await readStdin(stdin);
13
13
  if (!raw.trim())
14
14
  return;
15
- let parsed;
16
- try {
17
- parsed = JSON.parse(raw);
18
- }
19
- catch (error) {
20
- if (error instanceof SyntaxError)
21
- return;
22
- throw error;
23
- }
15
+ const parsed = JSON.parse(raw);
24
16
  const input = isRecord(parsed) ? parsed : {};
25
17
  const output = await runHook(input);
26
18
  if (output)
27
19
  process.stdout.write(output);
28
20
  }
21
+ catch {
22
+ // LSP feedback is best-effort: stderr or a non-zero exit here surfaces as harness noise on every edit.
23
+ return;
24
+ }
29
25
  finally {
30
- await disposeDefaultLspManager();
26
+ await disposeDefaultLspManager().catch(() => undefined);
31
27
  }
32
28
  }
33
29
  async function readStdin(stdin) {
@@ -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": "LazyCodex(4.9.2): Checking LSP Diagnostics"
11
+ "statusMessage": "LazyCodex(4.10.0): 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": "LazyCodex(4.9.2): Resetting LSP Diagnostics Cache"
24
+ "statusMessage": "LazyCodex(4.10.0): 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.9.2",
3
+ "version": "4.10.0",
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",
@@ -53,10 +53,10 @@
53
53
  "@code-yeongyu/lsp-daemon": "file:../../../../lsp-daemon"
54
54
  },
55
55
  "devDependencies": {
56
- "@biomejs/biome": "2.4.15",
57
- "@types/node": "^25.7.0",
56
+ "@biomejs/biome": "2.4.16",
57
+ "@types/node": "^25.9.3",
58
58
  "typescript": "^6.0.3",
59
- "vitest": "^4.1.5"
59
+ "vitest": "^4.1.8"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=20.0.0"
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, utimes, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { delimiter, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import test from "node:test";
7
7
 
@@ -18,10 +18,15 @@ async function makeFixture() {
18
18
  const fakeBin = join(root, "bin");
19
19
  await mkdir(fakeBin, { recursive: true });
20
20
  const npmLog = join(root, "npm.log");
21
+ await writeFile(
22
+ join(fakeBin, "npm.js"),
23
+ `const { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(npmLog)}, process.argv.slice(2).join(" ") + "\\n");\n`,
24
+ );
21
25
  await writeFile(
22
26
  join(fakeBin, "npm"),
23
- `#!/usr/bin/env node\nconst { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(npmLog)}, process.argv.slice(2).join(" ") + "\\n");\n`,
27
+ `#!/usr/bin/env node\nrequire("./npm.js");\n`,
24
28
  );
29
+ await writeFile(join(fakeBin, "npm.cmd"), `@echo off\r\nnode "%~dp0\\npm.js" %*\r\n`);
25
30
  await chmod(join(fakeBin, "npm"), 0o755);
26
31
  return { root, npmLog, script: join(root, "packages", "omo-codex", "plugin", "components", "lsp", "scripts", "build-lsp-tools.mjs"), fakeBin };
27
32
  }
@@ -29,7 +34,7 @@ async function makeFixture() {
29
34
  function runScript(script, fakeBin, args = []) {
30
35
  return spawnSync(process.execPath, [script, ...args], {
31
36
  encoding: "utf8",
32
- env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH ?? ""}` },
37
+ env: { ...process.env, PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}` },
33
38
  });
34
39
  }
35
40