oh-my-opencode 4.9.1 → 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 (220) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +1 -0
  2. package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
  3. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
  4. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
  5. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
  6. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
  7. package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
  8. package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  9. package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  10. package/bin/platform.js +5 -0
  11. package/bin/platform.test.ts +56 -0
  12. package/dist/agents/atlas/agent.d.ts +4 -3
  13. package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
  14. package/dist/agents/hephaestus/agent.d.ts +5 -0
  15. package/dist/agents/hephaestus/index.d.ts +1 -1
  16. package/dist/agents/metis.d.ts +1 -0
  17. package/dist/agents/prometheus/system-prompt.d.ts +1 -1
  18. package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
  19. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  20. package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
  21. package/dist/agents/types.d.ts +2 -2
  22. package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
  23. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
  24. package/dist/cli/doctor/constants.d.ts +1 -1
  25. package/dist/cli/index.js +32329 -31437
  26. package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
  27. package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
  28. package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
  29. package/dist/cli/model-fallback.d.ts +1 -0
  30. package/dist/cli/provider-availability.d.ts +2 -0
  31. package/dist/cli-node/index.js +32329 -31437
  32. package/dist/config/schema/agent-overrides.d.ts +80 -16
  33. package/dist/config/schema/experimental.d.ts +1 -1
  34. package/dist/config/schema/hooks.d.ts +0 -1
  35. package/dist/config/schema/internal/permission.d.ts +5 -1
  36. package/dist/config/schema/oh-my-opencode-config.d.ts +76 -16
  37. package/dist/create-hooks.d.ts +0 -1
  38. package/dist/features/background-agent/index.d.ts +1 -1
  39. package/dist/features/background-agent/manager.d.ts +6 -0
  40. package/dist/features/background-agent/types.d.ts +2 -0
  41. package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
  42. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  43. package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
  44. package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
  45. package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
  46. package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
  47. package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
  48. package/dist/features/tool-metadata-store/store.d.ts +5 -0
  49. package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
  50. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
  51. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
  52. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
  53. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
  54. package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
  55. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  56. package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
  57. package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
  58. package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
  59. package/dist/hooks/index.d.ts +0 -1
  60. package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
  61. package/dist/index.js +6061 -3714
  62. package/dist/oh-my-opencode.schema.json +123 -18
  63. package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
  64. package/dist/plugin/event-session-lifecycle.d.ts +0 -3
  65. package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
  66. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  67. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  68. package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
  69. package/dist/shared/internal-initiator-marker.d.ts +7 -0
  70. package/dist/shared/live-server-route.d.ts +24 -0
  71. package/dist/shared/plugin-identity.d.ts +2 -2
  72. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
  73. package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
  74. package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
  75. package/dist/shared/tmux/tmux-utils.d.ts +1 -0
  76. package/dist/testing/create-plugin-module.d.ts +4 -0
  77. package/dist/tools/background-task/clients.d.ts +2 -0
  78. package/dist/tools/background-task/full-session-format.d.ts +1 -0
  79. package/dist/tools/background-task/types.d.ts +1 -0
  80. package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
  81. package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
  82. package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
  83. package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
  84. package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
  85. package/dist/tools/look-at/types.d.ts +2 -0
  86. package/dist/tools/session-manager/types.d.ts +1 -0
  87. package/dist/tools/skill-mcp/types.d.ts +1 -0
  88. package/package.json +14 -13
  89. package/packages/ast-grep-mcp/dist/cli.js +50 -17
  90. package/packages/lsp-daemon/dist/cli.js +8 -5
  91. package/packages/lsp-daemon/dist/index.js +8 -5
  92. package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
  93. package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
  94. package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
  95. package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
  96. package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
  97. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  98. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
  99. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
  100. package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
  101. package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
  102. package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
  103. package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
  104. package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
  105. package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
  106. package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
  107. package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
  108. package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
  109. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
  110. package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
  111. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
  112. package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
  113. package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
  114. package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
  115. package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
  116. package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
  117. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  118. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  119. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  120. package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
  122. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  123. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  124. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
  125. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
  126. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
  127. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
  128. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  129. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  130. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
  131. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  132. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +4 -4
  133. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  134. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  135. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
  136. package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
  137. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  138. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  139. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
  140. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
  141. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
  142. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
  143. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
  144. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
  145. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
  147. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  148. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  149. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
  150. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
  151. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
  153. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
  155. package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
  156. package/packages/omo-codex/plugin/package-lock.json +193 -193
  157. package/packages/omo-codex/plugin/package.json +1 -1
  158. package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
  159. package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
  160. package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
  161. package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
  162. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  163. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  164. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
  165. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  166. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
  167. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
  168. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
  169. package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
  170. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
  171. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
  172. package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
  173. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
  174. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
  175. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
  176. package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
  177. package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
  178. package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
  179. package/packages/omo-codex/scripts/install/agents.mjs +78 -5
  180. package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
  181. package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
  182. package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
  183. package/packages/omo-codex/scripts/install/config.d.mts +35 -0
  184. package/packages/omo-codex/scripts/install/config.mjs +13 -3
  185. package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
  186. package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
  187. package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
  188. package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
  189. package/packages/omo-codex/scripts/install-local.mjs +3 -2
  190. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  191. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  192. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
  193. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  194. package/dist/hooks/session-recovery/constants.d.ts +0 -4
  195. package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
  196. package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
  197. package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
  198. package/dist/hooks/session-recovery/hook.d.ts +0 -4
  199. package/dist/hooks/session-recovery/index.d.ts +0 -5
  200. package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
  201. package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
  202. package/dist/hooks/session-recovery/message-state.d.ts +0 -4
  203. package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
  204. package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
  205. package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
  206. package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
  207. package/dist/hooks/session-recovery/resume.d.ts +0 -7
  208. package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
  209. package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
  210. package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
  211. package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
  212. package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
  213. package/dist/hooks/session-recovery/storage.d.ts +0 -20
  214. package/dist/plugin/event-session-recovery.d.ts +0 -9
  215. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
  216. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
  217. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
  218. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
  219. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
  220. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
@@ -0,0 +1,108 @@
1
+ import { spawn } from "node:child_process";
2
+ import { stat } from "node:fs/promises";
3
+ import type { Readable } from "node:stream";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { DEFAULT_LOCK_STALE_MS } from "../../../scripts/auto-update-state.mjs";
7
+ import { resolveBootstrapLockPath, resolveBootstrapStatePath } from "./environment.ts";
8
+ import { readBootstrapState, readPluginVersion } from "./worker.ts";
9
+
10
+ export const BOOTSTRAP_RESTART_NOTICE =
11
+ "LazyCodex bootstrap running in background — restart the session when it completes";
12
+
13
+ export type SessionStartAction =
14
+ | "spawned"
15
+ | "skip-completed"
16
+ | "skip-locked"
17
+ | "skip-missing-env"
18
+ | "skip-version-unresolved";
19
+
20
+ export interface WorkerSpawnInvocation {
21
+ readonly command: string;
22
+ readonly args: readonly string[];
23
+ readonly env: Record<string, string | undefined>;
24
+ }
25
+
26
+ export interface SessionStartHookOptions {
27
+ readonly env: Record<string, string | undefined>;
28
+ readonly stdin?: Readable & { readonly isTTY?: boolean };
29
+ readonly now?: number;
30
+ readonly spawnWorker?: (invocation: WorkerSpawnInvocation) => void;
31
+ readonly workerCliPath?: string;
32
+ readonly writeNotice?: (line: string) => void;
33
+ }
34
+
35
+ export interface SessionStartHookResult {
36
+ readonly exitCode: 0;
37
+ readonly action: SessionStartAction;
38
+ }
39
+
40
+ export async function runSessionStartHook(options: SessionStartHookOptions): Promise<number> {
41
+ return (await executeSessionStartHook(options)).exitCode;
42
+ }
43
+
44
+ export async function executeSessionStartHook(options: SessionStartHookOptions): Promise<SessionStartHookResult> {
45
+ if (options.stdin !== undefined) await drainStdin(options.stdin);
46
+ const now = options.now ?? Date.now();
47
+ const pluginRoot = options.env["PLUGIN_ROOT"]?.trim();
48
+ const pluginData = options.env["PLUGIN_DATA"]?.trim();
49
+ if (pluginRoot === undefined || pluginRoot.length === 0 || pluginData === undefined || pluginData.length === 0) {
50
+ return { action: "skip-missing-env", exitCode: 0 };
51
+ }
52
+
53
+ const pluginVersion = await readPluginVersion(pluginRoot);
54
+ if (pluginVersion === undefined) return { action: "skip-version-unresolved", exitCode: 0 };
55
+
56
+ const state = await readBootstrapState(resolveBootstrapStatePath(pluginData));
57
+ if (state.completedForVersion === pluginVersion) return { action: "skip-completed", exitCode: 0 };
58
+
59
+ if (await isLockFresh(resolveBootstrapLockPath(pluginData), now)) return { action: "skip-locked", exitCode: 0 };
60
+
61
+ const spawnWorker = options.spawnWorker ?? spawnDetachedWorker;
62
+ spawnWorker({
63
+ args: [options.workerCliPath ?? defaultWorkerCliPath(), "worker"],
64
+ command: process.execPath,
65
+ env: options.env,
66
+ });
67
+ const writeNotice = options.writeNotice ?? ((line: string) => process.stdout.write(`${line}\n`));
68
+ writeNotice(
69
+ JSON.stringify({
70
+ hookSpecificOutput: {
71
+ hookEventName: "SessionStart",
72
+ additionalContext: BOOTSTRAP_RESTART_NOTICE,
73
+ },
74
+ }),
75
+ );
76
+ return { action: "spawned", exitCode: 0 };
77
+ }
78
+
79
+ function spawnDetachedWorker(invocation: WorkerSpawnInvocation): void {
80
+ const child = spawn(invocation.command, [...invocation.args], {
81
+ detached: true,
82
+ env: invocation.env,
83
+ stdio: "ignore",
84
+ });
85
+ child.unref();
86
+ }
87
+
88
+ function defaultWorkerCliPath(): string {
89
+ // In the esbuild bundle every module shares import.meta.url, so this
90
+ // resolves to dist/cli.js — the file the detached worker must re-enter.
91
+ return fileURLToPath(import.meta.url);
92
+ }
93
+
94
+ async function isLockFresh(lockPath: string, now: number): Promise<boolean> {
95
+ try {
96
+ const lockStat = await stat(lockPath);
97
+ return now - lockStat.mtimeMs < DEFAULT_LOCK_STALE_MS;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ async function drainStdin(stdin: NonNullable<SessionStartHookOptions["stdin"]>): Promise<void> {
104
+ if (stdin.isTTY === true) return;
105
+ for await (const chunk of stdin) {
106
+ void chunk;
107
+ }
108
+ }
@@ -0,0 +1,243 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { inflateRawSync } from "node:zlib";
7
+
8
+ // Cross-package source import (bundled into dist/cli.js at build time) so the
9
+ // preexisting-sg probe order can never drift from the MCP's own resolver.
10
+ import { findSgCliPathSync } from "../../../../../ast-grep-mcp/src/sg-cli-path.ts";
11
+ import { downloadChecksummedAsset, loadAssetManifest } from "./download.ts";
12
+ import type { FetchLike } from "./download.ts";
13
+ import { appendBootstrapLog, BOOTSTRAP_DOCTOR_HINT } from "./worker.ts";
14
+ import type { BootstrapStepOutcome, BootstrapWorkerContext } from "./worker.ts";
15
+
16
+ export const SG_PROVISION_COMPONENT = "ast_grep";
17
+ export const SG_FORCE_PROVISION_ENV_KEY = "OMO_BOOTSTRAP_FORCE_PROVISION";
18
+ const SG_MANIFEST_NAME = "ast-grep";
19
+
20
+ export interface ResolvePreexistingSgOptions {
21
+ readonly arch: string;
22
+ readonly codexHome: string;
23
+ readonly env: Record<string, string | undefined>;
24
+ readonly platform: NodeJS.Platform;
25
+ }
26
+
27
+ export interface SgProvisionSeams {
28
+ readonly arch?: string;
29
+ readonly fetchImpl?: FetchLike;
30
+ readonly resolvePreexistingSg?: (options: ResolvePreexistingSgOptions) => string | null;
31
+ readonly runVersionProbe?: (binaryPath: string) => Promise<string>;
32
+ }
33
+
34
+ export function sgProvisionDestination(context: BootstrapWorkerContext, arch: string): string {
35
+ const binaryName = context.platform === "win32" ? "sg.exe" : "sg";
36
+ return join(context.codexHome, "runtime", "ast-grep", `${context.platform}-${arch}`, binaryName);
37
+ }
38
+
39
+ export async function runSgProvision(
40
+ context: BootstrapWorkerContext,
41
+ seams: SgProvisionSeams = {},
42
+ ): Promise<BootstrapStepOutcome> {
43
+ const arch = seams.arch ?? process.arch;
44
+ const destination = sgProvisionDestination(context, arch);
45
+
46
+ if (context.env[SG_FORCE_PROVISION_ENV_KEY] !== "1") {
47
+ const preexisting = (seams.resolvePreexistingSg ?? defaultResolvePreexistingSg)({
48
+ arch,
49
+ codexHome: context.codexHome,
50
+ env: context.env,
51
+ platform: context.platform,
52
+ });
53
+ if (preexisting !== null) {
54
+ await appendBootstrapLog(context.pluginData, context.now, "sg-provision", { sg: `preexisting:${preexisting}` });
55
+ return { degraded: [] };
56
+ }
57
+ }
58
+
59
+ const stagingDir = join(dirname(destination), `.staging-${randomUUID().slice(0, 8)}`);
60
+ try {
61
+ const version = await provisionFromManifest(context, seams, { arch, destination, stagingDir });
62
+ await appendBootstrapLog(context.pluginData, context.now, "sg-provision", {
63
+ sg: `provisioned:${destination}`,
64
+ version,
65
+ });
66
+ return { degraded: [] };
67
+ } catch (error) {
68
+ const reason = error instanceof Error ? error.message : String(error);
69
+ await appendBootstrapLog(context.pluginData, context.now, "sg-provision-failed", { reason });
70
+ return { degraded: [{ component: SG_PROVISION_COMPONENT, hint: BOOTSTRAP_DOCTOR_HINT, reason }] };
71
+ } finally {
72
+ await rm(stagingDir, { force: true, recursive: true });
73
+ }
74
+ }
75
+
76
+ async function provisionFromManifest(
77
+ context: BootstrapWorkerContext,
78
+ seams: SgProvisionSeams,
79
+ layout: { readonly arch: string; readonly destination: string; readonly stagingDir: string },
80
+ ): Promise<string> {
81
+ const manifest = await loadAssetManifest(SG_MANIFEST_NAME, context.flags.manifestDir);
82
+ const platformKey = `${context.platform}-${layout.arch}`;
83
+ const asset = manifest.platforms[platformKey];
84
+ if (asset === undefined) {
85
+ throw new Error(
86
+ `ast-grep ${manifest.version} has no asset for unsupported platform "${platformKey}" (available: ${Object.keys(manifest.platforms).join(", ")}).`,
87
+ );
88
+ }
89
+
90
+ await mkdir(layout.stagingDir, { recursive: true });
91
+ const archivePath = await downloadChecksummedAsset({
92
+ destination: join(layout.stagingDir, basename(new URL(asset.url).pathname)),
93
+ env: context.env as NodeJS.ProcessEnv,
94
+ sha256: asset.sha256,
95
+ url: asset.url,
96
+ ...(seams.fetchImpl === undefined ? {} : { fetchImpl: seams.fetchImpl }),
97
+ });
98
+
99
+ const binaryBytes = extractStandaloneSgBinary(await readFile(archivePath), context.platform);
100
+ const stagedBinary = join(layout.stagingDir, basename(layout.destination));
101
+ await writeFile(stagedBinary, binaryBytes);
102
+ await chmod(stagedBinary, 0o755);
103
+ await rename(stagedBinary, layout.destination);
104
+
105
+ await verifyProvisionedVersion(layout.destination, manifest.version, seams);
106
+ return manifest.version;
107
+ }
108
+
109
+ async function verifyProvisionedVersion(
110
+ destination: string,
111
+ pinnedVersion: string,
112
+ seams: SgProvisionSeams,
113
+ ): Promise<void> {
114
+ let reported: string;
115
+ try {
116
+ reported = (await (seams.runVersionProbe ?? defaultVersionProbe)(destination)).trim();
117
+ } catch (error) {
118
+ await rm(destination, { force: true });
119
+ throw new Error(
120
+ `provisioned sg at ${destination} failed its --version probe: ${error instanceof Error ? error.message : String(error)}`,
121
+ );
122
+ }
123
+ if (!reported.includes(pinnedVersion)) {
124
+ await rm(destination, { force: true });
125
+ throw new Error(
126
+ `provisioned sg at ${destination} reported "${reported}" but the manifest pins version ${pinnedVersion}; removed the binary.`,
127
+ );
128
+ }
129
+ }
130
+
131
+ function defaultResolvePreexistingSg(options: ResolvePreexistingSgOptions): string | null {
132
+ return findSgCliPathSync({
133
+ arch: options.arch,
134
+ env: { ...options.env, CODEX_HOME: options.codexHome },
135
+ platform: options.platform,
136
+ });
137
+ }
138
+
139
+ const execFileAsync = promisify(execFile);
140
+
141
+ async function defaultVersionProbe(binaryPath: string): Promise<string> {
142
+ const { stdout } = await execFileAsync(binaryPath, ["--version"]);
143
+ return String(stdout);
144
+ }
145
+
146
+ // Release zips (verified against 0.42.3) contain two entries: a tiny `sg`
147
+ // alias shim that exec's `ast-grep` via PATH search ONLY (it fails standalone
148
+ // even with a sibling ast-grep), and the standalone `ast-grep` binary. The
149
+ // standalone entry is therefore installed under the destination name sg[.exe].
150
+ function extractStandaloneSgBinary(zip: Buffer, platform: NodeJS.Platform): Buffer {
151
+ const suffix = platform === "win32" ? ".exe" : "";
152
+ const entries = listZipEntries(zip);
153
+ const preferredNames = [`ast-grep${suffix}`, `sg${suffix}`];
154
+ for (const preferred of preferredNames) {
155
+ const entry = entries.find((candidate) => zipEntryBaseName(candidate.name) === preferred);
156
+ if (entry !== undefined) return readZipEntryBytes(zip, entry);
157
+ }
158
+ throw new Error(
159
+ `ast-grep release zip has no ${preferredNames.join(" or ")} entry (found: ${entries.map((entry) => entry.name).join(", ")}).`,
160
+ );
161
+ }
162
+
163
+ function zipEntryBaseName(entryName: string): string {
164
+ const segments = entryName.split("/");
165
+ return segments[segments.length - 1] ?? entryName;
166
+ }
167
+
168
+ interface ZipCentralEntry {
169
+ readonly compressedSize: number;
170
+ readonly localHeaderOffset: number;
171
+ readonly method: number;
172
+ readonly name: string;
173
+ readonly uncompressedSize: number;
174
+ }
175
+
176
+ // Minimal zip reader (EOCD -> central directory -> local header) so extraction
177
+ // stays pure-node and deterministic; offsets follow APPNOTE.TXT 4.3.
178
+ const EOCD_SIGNATURE = 0x06054b50;
179
+ const CENTRAL_SIGNATURE = 0x02014b50;
180
+ const LOCAL_SIGNATURE = 0x04034b50;
181
+ const ZIP64_SENTINEL = 0xffffffff;
182
+
183
+ function listZipEntries(zip: Buffer): ZipCentralEntry[] {
184
+ const eocdOffset = findEndOfCentralDirectory(zip);
185
+ const entryCount = zip.readUInt16LE(eocdOffset + 10);
186
+ let cursor = zip.readUInt32LE(eocdOffset + 16);
187
+ const entries: ZipCentralEntry[] = [];
188
+ for (let index = 0; index < entryCount; index += 1) {
189
+ if (cursor + 46 > zip.length || zip.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
190
+ throw new Error("zip central directory is corrupt (bad entry signature)");
191
+ }
192
+ const nameLength = zip.readUInt16LE(cursor + 28);
193
+ const extraLength = zip.readUInt16LE(cursor + 30);
194
+ const commentLength = zip.readUInt16LE(cursor + 32);
195
+ entries.push({
196
+ compressedSize: zip.readUInt32LE(cursor + 20),
197
+ localHeaderOffset: zip.readUInt32LE(cursor + 42),
198
+ method: zip.readUInt16LE(cursor + 10),
199
+ name: zip.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8"),
200
+ uncompressedSize: zip.readUInt32LE(cursor + 24),
201
+ });
202
+ cursor += 46 + nameLength + extraLength + commentLength;
203
+ }
204
+ return entries;
205
+ }
206
+
207
+ function findEndOfCentralDirectory(zip: Buffer): number {
208
+ const lowestOffset = Math.max(0, zip.length - 22 - 65_535);
209
+ for (let offset = zip.length - 22; offset >= lowestOffset; offset -= 1) {
210
+ if (zip.readUInt32LE(offset) === EOCD_SIGNATURE) return offset;
211
+ }
212
+ throw new Error("downloaded asset is not a zip archive (end-of-central-directory record missing)");
213
+ }
214
+
215
+ function readZipEntryBytes(zip: Buffer, entry: ZipCentralEntry): Buffer {
216
+ if (
217
+ entry.compressedSize === ZIP64_SENTINEL ||
218
+ entry.uncompressedSize === ZIP64_SENTINEL ||
219
+ entry.localHeaderOffset === ZIP64_SENTINEL
220
+ ) {
221
+ throw new Error(`zip entry ${entry.name} uses unsupported zip64 extensions`);
222
+ }
223
+ if (zip.readUInt32LE(entry.localHeaderOffset) !== LOCAL_SIGNATURE) {
224
+ throw new Error(`zip entry ${entry.name} has a corrupt local header`);
225
+ }
226
+ const nameLength = zip.readUInt16LE(entry.localHeaderOffset + 26);
227
+ const extraLength = zip.readUInt16LE(entry.localHeaderOffset + 28);
228
+ const dataStart = entry.localHeaderOffset + 30 + nameLength + extraLength;
229
+ const raw = zip.subarray(dataStart, dataStart + entry.compressedSize);
230
+ const bytes = decompressZipEntry(raw, entry);
231
+ if (bytes.length !== entry.uncompressedSize) {
232
+ throw new Error(
233
+ `zip entry ${entry.name} inflated to ${bytes.length} bytes but the archive declares ${entry.uncompressedSize}`,
234
+ );
235
+ }
236
+ return bytes;
237
+ }
238
+
239
+ function decompressZipEntry(raw: Buffer, entry: ZipCentralEntry): Buffer {
240
+ if (entry.method === 0) return Buffer.from(raw);
241
+ if (entry.method === 8) return inflateRawSync(raw);
242
+ throw new Error(`zip entry ${entry.name} uses unsupported compression method ${entry.method}`);
243
+ }
@@ -0,0 +1,294 @@
1
+ import { execFile } from "node:child_process";
2
+ import { copyFile, mkdir, readdir, rm, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ // These relative imports resolve at BUILD time in the monorepo; esbuild
7
+ // inlines the install modules into dist/cli.js so PLUGIN_ROOT ships nothing
8
+ // beyond the bundle.
9
+ import {
10
+ capturePreservedAgentReasoning,
11
+ capturePreservedAgentServiceTier,
12
+ linkCachedPluginAgents,
13
+ } from "../../../../scripts/install/agents.mjs";
14
+ import { resolveCodexInstallerBinDir } from "../../../../scripts/install/bin-dir.mjs";
15
+ import { linkCachedPluginBins, linkRootRuntimeBin } from "../../../../scripts/install/bin-links.mjs";
16
+ import { updateCodexConfig } from "../../../../scripts/install/config.mjs";
17
+ import type { CodexAgentConfig } from "../../../../scripts/install/config.mjs";
18
+ import { stampGitBashMcpEnv } from "../../../../scripts/install/git-bash-mcp-env.mjs";
19
+ import { prepareGitBashForInstall } from "../../../../scripts/install/git-bash.mjs";
20
+ import type { GitBashResolution } from "../../../../scripts/install/git-bash.mjs";
21
+ import { trustedHookStatesForPlugin } from "../../../../scripts/install/hook-trust.mjs";
22
+ import { appendBootstrapLog, BOOTSTRAP_DOCTOR_HINT } from "./worker.ts";
23
+ import type { BootstrapDegradedEntry, BootstrapStepOutcome } from "./worker.ts";
24
+
25
+ export const SETUP_MARKETPLACE_NAME = "sisyphuslabs";
26
+ export const SETUP_PLUGIN_NAME = "omo";
27
+ export const GIT_BASH_INSTALL_HINT = "winget install --id Git.Git -e --source winget";
28
+
29
+ export type SetupRunCommand = (command: string, args: readonly string[], options: { cwd: string }) => Promise<unknown>;
30
+
31
+ export interface WorkerSetupOptions {
32
+ readonly codexHome: string;
33
+ readonly env: Record<string, string | undefined>;
34
+ readonly pluginData: string;
35
+ readonly pluginRoot: string;
36
+ readonly platform: NodeJS.Platform;
37
+ /** Timestamp used for bootstrap.log entries; the worker passes its run time. */
38
+ readonly now?: number;
39
+ /** Test seam: command runner for the win32 Git Bash auto-install. */
40
+ readonly runCommand?: SetupRunCommand;
41
+ /** Test seam: overrides Git Bash discovery (win32 only). */
42
+ readonly resolveGitBash?: () => GitBashResolution;
43
+ }
44
+
45
+ interface AgentLinkOutcome {
46
+ readonly agentConfigs: readonly CodexAgentConfig[];
47
+ readonly degraded: readonly BootstrapDegradedEntry[];
48
+ }
49
+
50
+ // Worker setup sequence (every sub-step is idempotent and degraded-not-fatal,
51
+ // unlike the npx installer which throws): Git Bash preflight -> bundled agent
52
+ // TOML linking -> config blocks + hook trust re-stamp -> git_bash MCP env ->
53
+ // version-aware bin links. Bin linking re-runs whenever the worker re-runs,
54
+ // and the worker re-runs whenever completedForVersion changes (Task 7 marker
55
+ // semantics), so links always point at the CURRENT versioned PLUGIN_ROOT even
56
+ // though Codex deletes old version dirs on upgrade (core-plugins store.rs).
57
+ export async function runWorkerSetup(options: WorkerSetupOptions): Promise<BootstrapStepOutcome> {
58
+ const degraded: BootstrapDegradedEntry[] = [];
59
+ const gitBashEnabled = await resolveGitBashStep(options, degraded);
60
+ const agents = await linkBundledAgentsStep(options);
61
+ degraded.push(...agents.degraded);
62
+ await updateConfigStep(options, { agentConfigs: agents.agentConfigs, gitBashEnabled }, degraded);
63
+ await stampGitBashEnvStep(options, degraded);
64
+ await linkComponentBinsStep(options, degraded);
65
+ return { degraded };
66
+ }
67
+
68
+ async function resolveGitBashStep(options: WorkerSetupOptions, degraded: BootstrapDegradedEntry[]): Promise<boolean> {
69
+ if (options.platform !== "win32") return false;
70
+ try {
71
+ const resolution = await prepareGitBashForInstall({
72
+ cwd: options.pluginRoot,
73
+ env: options.env,
74
+ platform: options.platform,
75
+ runCommand: options.runCommand ?? defaultRunCommand,
76
+ ...(options.resolveGitBash === undefined ? {} : { resolveGitBash: options.resolveGitBash }),
77
+ });
78
+ if (resolution.found) return true;
79
+ degraded.push({
80
+ component: "git-bash",
81
+ hint: GIT_BASH_INSTALL_HINT,
82
+ reason: "Git Bash was not found on this Windows machine; the omo git_bash MCP server stays disabled",
83
+ });
84
+ } catch (error) {
85
+ degraded.push({
86
+ component: "git-bash",
87
+ hint: GIT_BASH_INSTALL_HINT,
88
+ reason: `Git Bash preflight failed: ${errorMessage(error)}`,
89
+ });
90
+ }
91
+ return false;
92
+ }
93
+
94
+ async function linkBundledAgentsStep(options: WorkerSetupOptions): Promise<AgentLinkOutcome> {
95
+ const agentsTarget = join(options.codexHome, "agents");
96
+ try {
97
+ // linkCachedPluginAgents writes its .installed-agents.json manifest next
98
+ // to the agent sources, so the bundled TOMLs are staged under PLUGIN_DATA
99
+ // first: bootstrap must never persist anything under PLUGIN_ROOT (the
100
+ // Codex-managed marketplace cache).
101
+ const stageRoot = join(options.pluginData, "bootstrap", "agents-stage");
102
+ await stageBundledAgents(options.pluginRoot, stageRoot);
103
+ const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
104
+ const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
105
+ const linked = await linkCachedPluginAgents({
106
+ codexHome: options.codexHome,
107
+ pluginRoot: stageRoot,
108
+ preservedReasoning,
109
+ preservedServiceTier,
110
+ });
111
+ const agentConfigs = linked
112
+ .map((link) => ({ configFile: `./agents/${link.name}`, name: agentNameFromToml(link.name) }))
113
+ .sort((left, right) => left.name.localeCompare(right.name));
114
+ return { agentConfigs, degraded: [] };
115
+ } catch (error) {
116
+ return {
117
+ agentConfigs: [],
118
+ degraded: [
119
+ {
120
+ component: "agents",
121
+ hint: BOOTSTRAP_DOCTOR_HINT,
122
+ reason: `failed to link bundled agents into ${agentsTarget}: ${errorMessage(error)}`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ }
128
+
129
+ async function stageBundledAgents(pluginRoot: string, stageRoot: string): Promise<void> {
130
+ await rm(stageRoot, { force: true, recursive: true });
131
+ await mkdir(stageRoot, { recursive: true });
132
+ const componentsRoot = join(pluginRoot, "components");
133
+ for (const componentName of await directoryNames(componentsRoot)) {
134
+ const agentsDir = join(componentsRoot, componentName, "agents");
135
+ const agentFiles = (await fileNames(agentsDir)).filter((name) => name.endsWith(".toml"));
136
+ if (agentFiles.length === 0) continue;
137
+ const stagedAgentsDir = join(stageRoot, "components", componentName, "agents");
138
+ await mkdir(stagedAgentsDir, { recursive: true });
139
+ for (const agentFile of agentFiles) {
140
+ await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
141
+ }
142
+ }
143
+ }
144
+
145
+ async function updateConfigStep(
146
+ options: WorkerSetupOptions,
147
+ inputs: { agentConfigs: readonly CodexAgentConfig[]; gitBashEnabled: boolean },
148
+ degraded: BootstrapDegradedEntry[],
149
+ ): Promise<void> {
150
+ const configPath = join(options.codexHome, "config.toml");
151
+ try {
152
+ await assertWritableConfigIfPresent(configPath);
153
+ // Re-stamping trusted hook hashes after an upgrade is what makes the
154
+ // next session's hooks trusted again once the user re-approved the
155
+ // bootstrap hook itself.
156
+ const trustedHookStates = await trustedHookStatesForPlugin({
157
+ marketplaceName: SETUP_MARKETPLACE_NAME,
158
+ pluginName: SETUP_PLUGIN_NAME,
159
+ pluginRoot: options.pluginRoot,
160
+ });
161
+ await updateCodexConfig({
162
+ agentConfigs: inputs.agentConfigs,
163
+ // Hard invariant: the bootstrap worker NEVER writes permission keys
164
+ // (approval/sandbox/network policies stay installer-flag-only).
165
+ autonomousPermissions: false,
166
+ configPath,
167
+ gitBashEnabled: inputs.gitBashEnabled,
168
+ marketplaceName: SETUP_MARKETPLACE_NAME,
169
+ platform: options.platform,
170
+ pluginNames: [SETUP_PLUGIN_NAME],
171
+ preserveMarketplaceSource: true,
172
+ // The marketplace plugin tree has no <root>/plugin/model-catalog.json,
173
+ // so updateCodexConfig falls back to the catalog bundled into this
174
+ // dist; bootstrap-setup.test.mjs guards against drift between the two.
175
+ repoRoot: options.pluginRoot,
176
+ trustedHookStates,
177
+ });
178
+ } catch (error) {
179
+ degraded.push({
180
+ component: "config",
181
+ hint: BOOTSTRAP_DOCTOR_HINT,
182
+ reason: `failed to update ${configPath}: ${errorMessage(error)}`,
183
+ });
184
+ }
185
+ }
186
+
187
+ async function assertWritableConfigIfPresent(configPath: string): Promise<void> {
188
+ try {
189
+ if (((await stat(configPath)).mode & 0o222) === 0) throw new Error(`${configPath} has no write permission bits set`);
190
+ } catch (error) {
191
+ if (errorCode(error) === "ENOENT") return;
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ function errorCode(error: unknown): string | undefined {
197
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
198
+ }
199
+
200
+ async function linkComponentBinsStep(options: WorkerSetupOptions, degraded: BootstrapDegradedEntry[]): Promise<void> {
201
+ const binDir = resolveCodexInstallerBinDir({ codexHome: options.codexHome, env: options.env });
202
+ try {
203
+ await linkCachedPluginBins({ binDir, pluginRoot: options.pluginRoot, platform: options.platform });
204
+ } catch (error) {
205
+ degraded.push({
206
+ component: "bin-links",
207
+ hint: BOOTSTRAP_DOCTOR_HINT,
208
+ reason: `failed to link component bins into ${binDir}: ${errorMessage(error)}`,
209
+ });
210
+ }
211
+ await linkRuntimeWrapperStep(options, binDir, degraded);
212
+ }
213
+
214
+ // The marketplace payload intentionally ships without <pluginRoot>/dist/cli
215
+ // (thin payload), so linkRootRuntimeBin returning null is the expected
216
+ // degraded mode there: record the omo-cli ledger entry and log the same
217
+ // warning install-local.mjs prints instead of leaving a broken `omo` link.
218
+ async function linkRuntimeWrapperStep(
219
+ options: WorkerSetupOptions,
220
+ binDir: string,
221
+ degraded: BootstrapDegradedEntry[],
222
+ ): Promise<void> {
223
+ const cliPath = join(options.pluginRoot, "dist", "cli", "index.js");
224
+ try {
225
+ const linked = await linkRootRuntimeBin({
226
+ binDir,
227
+ codexHome: options.codexHome,
228
+ platform: options.platform,
229
+ repoRoot: options.pluginRoot,
230
+ });
231
+ if (linked !== null) return;
232
+ degraded.push({
233
+ component: "omo-cli",
234
+ hint: "use npx lazycodex-ai for the omo CLI",
235
+ reason: "marketplace payload has no dist/cli",
236
+ });
237
+ await appendBootstrapLog(options.pluginData, options.now ?? Date.now(), "omo-cli-degraded", {
238
+ warning: `Warning: skipped the omo runtime wrapper because ${cliPath} is missing; omo sparkshell/ulw-loop commands will be unavailable until a package shipping dist/cli is installed`,
239
+ });
240
+ } catch (error) {
241
+ degraded.push({
242
+ component: "omo-cli",
243
+ hint: BOOTSTRAP_DOCTOR_HINT,
244
+ reason: `failed to link the omo runtime wrapper into ${binDir}: ${errorMessage(error)}`,
245
+ });
246
+ }
247
+ }
248
+
249
+ async function stampGitBashEnvStep(options: WorkerSetupOptions, degraded: BootstrapDegradedEntry[]): Promise<void> {
250
+ try {
251
+ await stampGitBashMcpEnv({ env: options.env, platform: options.platform, pluginRoot: options.pluginRoot });
252
+ } catch (error) {
253
+ degraded.push({
254
+ component: "git-bash-env",
255
+ hint: BOOTSTRAP_DOCTOR_HINT,
256
+ reason: `failed to stamp ${join(options.pluginRoot, ".mcp.json")}: ${errorMessage(error)}`,
257
+ });
258
+ }
259
+ }
260
+
261
+ const execFileAsync = promisify(execFile);
262
+
263
+ async function defaultRunCommand(command: string, args: readonly string[], options: { cwd: string }): Promise<unknown> {
264
+ return execFileAsync(command, [...args], { cwd: options.cwd });
265
+ }
266
+
267
+ async function directoryNames(root: string): Promise<string[]> {
268
+ return entryNames(root, (entry) => entry.isDirectory());
269
+ }
270
+
271
+ async function fileNames(root: string): Promise<string[]> {
272
+ return entryNames(root, (entry) => entry.isFile());
273
+ }
274
+
275
+ async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise<string[]> {
276
+ try {
277
+ const entries = await readdir(root, { withFileTypes: true });
278
+ return entries
279
+ .filter((entry) => keep(entry))
280
+ .map((entry) => entry.name)
281
+ .sort();
282
+ } catch (error) {
283
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
284
+ throw error;
285
+ }
286
+ }
287
+
288
+ function agentNameFromToml(fileName: string): string {
289
+ return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
290
+ }
291
+
292
+ function errorMessage(error: unknown): string {
293
+ return error instanceof Error ? error.message : String(error);
294
+ }