oh-my-opencode 4.5.12 → 4.6.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 (147) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/dist/cli/cleanup-command.d.ts +4 -0
  28. package/dist/cli/cleanup.d.ts +11 -0
  29. package/dist/cli/cli-program.d.ts +2 -1
  30. package/dist/cli/index.js +1837 -450
  31. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  32. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  33. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  34. package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
  35. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
  37. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  38. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  39. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  40. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  41. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  42. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  43. package/dist/cli/install-codex/index.d.ts +4 -0
  44. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  45. package/dist/cli/install-codex/types.d.ts +20 -0
  46. package/dist/cli/run/event-state.d.ts +1 -0
  47. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  48. package/dist/cli/run/prompt-start.d.ts +7 -0
  49. package/dist/cli/star-request.d.ts +9 -0
  50. package/dist/config/schema/hooks.d.ts +0 -1
  51. package/dist/create-hooks.d.ts +0 -1
  52. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  53. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/index.js +267 -114
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  57. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  58. package/dist/plugin/messages-transform.d.ts +8 -1
  59. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  60. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  61. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  62. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  63. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  64. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  65. package/dist/shared/prompt-async-gate.d.ts +1 -1
  66. package/package.json +22 -17
  67. package/packages/git-bash-mcp/dist/cli.js +367 -0
  68. package/packages/omo-codex/plugin/.mcp.json +11 -0
  69. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  70. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  71. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  72. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  73. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  74. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  75. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  76. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  77. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  78. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  79. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  80. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  81. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  82. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  83. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  84. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  85. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  86. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  87. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  88. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  89. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
  90. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  91. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  94. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
  96. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
  100. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  102. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  103. package/packages/omo-codex/plugin/package-lock.json +19 -0
  104. package/packages/omo-codex/plugin/package.json +3 -1
  105. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  106. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  107. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
  110. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
  111. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
  112. package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
  114. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  115. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  116. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  117. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  118. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
  119. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
  120. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  121. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  122. package/packages/omo-codex/scripts/install/config.mjs +36 -1
  123. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  124. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  125. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  126. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  127. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  128. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  129. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  130. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  131. package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
  132. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  133. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  134. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  135. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  136. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
  137. package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
  138. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
  139. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  140. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  141. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  142. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  143. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  144. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  145. package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
  146. package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
  147. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,145 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { installMarketplaceLocally } from "./install-local.mjs";
8
+
9
+ const windowsGitBashPath = "C:\\Program Files\\Git\\bin\\bash.exe";
10
+ const lspCliPath = join(process.cwd(), "packages", "lsp-tools-mcp", "dist", "cli.js");
11
+
12
+ async function withBundledLspRuntimeForTest(run) {
13
+ try {
14
+ await stat(lspCliPath);
15
+ } catch (error) {
16
+ if (!(error instanceof Error)) throw error;
17
+ await mkdir(join(process.cwd(), "packages", "lsp-tools-mcp", "dist"), { recursive: true });
18
+ await writeFile(lspCliPath, "#!/usr/bin/env node\n");
19
+ }
20
+
21
+ return run();
22
+ }
23
+
24
+ test("#given Windows without Git Bash and auto install skip env #when installing local marketplace #then rejects before marketplace or config mutation", async () => {
25
+ const repoRoot = await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-missing-repo-"));
26
+ const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-missing-home-"));
27
+ const commands = [];
28
+
29
+ await assert.rejects(
30
+ installMarketplaceLocally({
31
+ repoRoot,
32
+ codexHome,
33
+ platform: "win32",
34
+ env: { OMO_CODEX_SKIP_GIT_BASH_AUTO_INSTALL: "1" },
35
+ gitBashResolver: () => ({
36
+ found: false,
37
+ checkedPaths: [windowsGitBashPath],
38
+ installHint: [
39
+ "Git Bash is required.",
40
+ "winget install --id Git.Git -e --source winget",
41
+ "OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash.exe",
42
+ "rerun `npx lazycodex-ai install`",
43
+ ].join("\n"),
44
+ }),
45
+ runCommand: async (command, args, options) => {
46
+ commands.push([command, ...args, options.cwd].join(" "));
47
+ },
48
+ log: () => {},
49
+ }),
50
+ /winget install --id Git\.Git -e --source winget/,
51
+ );
52
+ assert.deepEqual(commands, []);
53
+ await assert.rejects(stat(join(codexHome, "config.toml")), /ENOENT/);
54
+ });
55
+
56
+ test("#given Windows without Git Bash #when winget succeeds and resolver recovers #then install continues", async () => {
57
+ const runCalls = [];
58
+ const resolutions = [
59
+ { found: false, checkedPaths: [windowsGitBashPath], installHint: "install hint before winget" },
60
+ { found: true, path: windowsGitBashPath, source: "program-files" },
61
+ ];
62
+ let resolveCallCount = 0;
63
+
64
+ const result = await withBundledLspRuntimeForTest(async () => installMarketplaceLocally({
65
+ repoRoot: process.cwd(),
66
+ codexHome: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-auto-home-")),
67
+ binDir: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-auto-bin-")),
68
+ platform: "win32",
69
+ gitBashResolver: () => resolutions[resolveCallCount++] ?? resolutions[resolutions.length - 1],
70
+ runCommand: async (command, args, options) => {
71
+ runCalls.push([command, ...args, options.cwd].join(" "));
72
+ },
73
+ log: () => {},
74
+ }));
75
+
76
+ assert.equal(resolveCallCount, 2);
77
+ assert.match(runCalls.join("\n"), /^winget install --id Git\.Git -e --source winget /m);
78
+ assert.equal(result.gitBashPath, windowsGitBashPath);
79
+ });
80
+
81
+ test("#given non-Windows install #when running installer #then winget is never called", async () => {
82
+ const runCalls = [];
83
+ const result = await withBundledLspRuntimeForTest(async () => installMarketplaceLocally({
84
+ repoRoot: process.cwd(),
85
+ codexHome: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-no-winget-home-")),
86
+ binDir: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-no-winget-bin-")),
87
+ platform: "linux",
88
+ runCommand: async (command, args, options) => {
89
+ runCalls.push([command, ...args, options.cwd].join(" "));
90
+ },
91
+ log: () => {},
92
+ }));
93
+
94
+ assert.equal(result.gitBashPath, null);
95
+ assert.equal(runCalls.some((command) => command.startsWith("winget ")), false);
96
+ });
97
+
98
+ test("#given Windows env override resolves Git Bash #when installing local marketplace #then install continues", async () => {
99
+ const result = await withBundledLspRuntimeForTest(async () => installMarketplaceLocally({
100
+ repoRoot: process.cwd(),
101
+ codexHome: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-home-")),
102
+ binDir: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-bin-")),
103
+ platform: "win32",
104
+ gitBashResolver: () => ({ found: true, path: windowsGitBashPath, source: "env" }),
105
+ runCommand: async () => {},
106
+ log: () => {},
107
+ }));
108
+
109
+ assert.equal(result.gitBashPath, windowsGitBashPath);
110
+ assert.equal(result.installed.length, 1);
111
+ });
112
+
113
+ test("#given Windows env override in installer options #when no custom resolver is provided #then default resolver uses it", async () => {
114
+ const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-env-home-"));
115
+ const gitBashPath = join(await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-env-")), "bash.exe");
116
+ await writeFile(gitBashPath, "");
117
+
118
+ const result = await withBundledLspRuntimeForTest(async () => installMarketplaceLocally({
119
+ repoRoot: process.cwd(),
120
+ codexHome,
121
+ binDir: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-env-bin-")),
122
+ platform: "win32",
123
+ env: { OMO_CODEX_GIT_BASH_PATH: gitBashPath },
124
+ runCommand: async () => {},
125
+ log: () => {},
126
+ }));
127
+
128
+ assert.equal(result.gitBashPath, gitBashPath);
129
+ });
130
+
131
+ test("#given non-Windows local install #when resolver would fail #then installer keeps existing behavior", async () => {
132
+ const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-linux-home-"));
133
+ const result = await withBundledLspRuntimeForTest(async () => installMarketplaceLocally({
134
+ repoRoot: process.cwd(),
135
+ codexHome,
136
+ binDir: await mkdtemp(join(tmpdir(), "omo-codex-script-git-bash-linux-bin-")),
137
+ platform: "linux",
138
+ gitBashResolver: () => ({ found: false, checkedPaths: [windowsGitBashPath], installHint: "should not be used" }),
139
+ runCommand: async () => {},
140
+ log: () => {},
141
+ }));
142
+
143
+ assert.equal(result.gitBashPath, null);
144
+ assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /\[marketplaces\.sisyphuslabs\]/);
145
+ });
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { realpathSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
 
7
8
  import {
@@ -12,6 +13,10 @@ import {
12
13
  } from "./install/cache.mjs";
13
14
  import { linkCachedPluginAgents } from "./install/agents.mjs";
14
15
  import { updateCodexConfig } from "./install/config.mjs";
16
+ import {
17
+ emptyProjectLocalCodexCleanupResult,
18
+ repairNearestProjectLocalCodexArtifacts,
19
+ } from "./install/project-local-cleanup.mjs";
15
20
  import { trustedHookStatesForPlugin } from "./install/hook-trust.mjs";
16
21
  import { defaultRunCommand } from "./install/process.mjs";
17
22
  import { writeInstalledMarketplaceSnapshot } from "./install/snapshot.mjs";
@@ -21,6 +26,10 @@ import {
21
26
  resolvePluginSource,
22
27
  validatePathSegment,
23
28
  } from "./install/marketplace.mjs";
29
+ import { prepareGitBashForInstall, resolveGitBashForCurrentProcess } from "./install/git-bash.mjs";
30
+ import { formatLazyCodexInstallHelp, parseLazyCodexInstallCliArgs } from "./install/cli-args.mjs";
31
+ import { runDelegatedOmoCommand } from "./install/delegated-command.mjs";
32
+ import { shouldBuildSourcePackages } from "./install/source-package-build.mjs";
24
33
 
25
34
  const LEGACY_CODEX_PLUGIN_MARKETPLACE = ["code", "yeongyu", "codex", "plugins"].join("-");
26
35
  const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", LEGACY_CODEX_PLUGIN_MARKETPLACE];
@@ -41,10 +50,24 @@ export async function installMarketplaceLocally(options = {}) {
41
50
  const env = options.env ?? process.env;
42
51
  const homeDir = resolve(options.homeDir ?? homedir());
43
52
  const codexHome = resolve(options.codexHome ?? nonEmptyEnvValue(env, "CODEX_HOME") ?? join(homeDir, ".codex"));
53
+ const projectDirectory = resolve(options.projectDirectory ?? nonEmptyEnvValue(env, "OMO_CODEX_PROJECT") ?? process.cwd());
44
54
  const binDir = resolve(options.binDir ?? resolveCodexInstallerBinDir({ codexHome, env, homeDir }));
45
55
  const platform = options.platform ?? process.platform;
46
56
  const runCommand = options.runCommand ?? defaultRunCommand;
47
57
  const log = options.log ?? console.log;
58
+ const buildSource = await shouldBuildSourcePackages(repoRoot);
59
+ const gitBashResolution = await prepareGitBashForInstall({
60
+ platform,
61
+ env,
62
+ cwd: repoRoot,
63
+ runCommand,
64
+ resolveGitBash: platform === "win32"
65
+ ? (options.gitBashResolver ?? (() => resolveGitBashForCurrentProcess({ platform, env })))
66
+ : undefined,
67
+ });
68
+ if (!gitBashResolution.found) {
69
+ throw new Error(gitBashResolution.installHint);
70
+ }
48
71
  const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
49
72
  const marketplace = await readMarketplace(repoRoot, {
50
73
  marketplacePath: join(codexPackageRoot, "marketplace.json"),
@@ -66,6 +89,7 @@ export async function installMarketplaceLocally(options = {}) {
66
89
 
67
90
  log(`Building ${entry.name}@${version}`);
68
91
  const plugin = await installCachedPlugin({
92
+ buildSource,
69
93
  codexHome,
70
94
  marketplaceName: marketplace.name,
71
95
  name: entry.name,
@@ -120,15 +144,38 @@ export async function installMarketplaceLocally(options = {}) {
120
144
  marketplaceName: marketplace.name,
121
145
  marketplaceSource: { sourceType: "local", source: marketplaceRoot },
122
146
  pluginNames,
147
+ platform,
123
148
  trustedHookStates,
124
149
  agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)),
150
+ autonomousPermissions: options.autonomousPermissions === true,
125
151
  });
152
+ const projectCleanup = await repairProjectLocalCodexArtifactsBestEffort({ startDirectory: projectDirectory, codexHome, log });
153
+ for (const configCleanup of projectCleanup.configs) {
154
+ if (!configCleanup.changed) continue;
155
+ log(`Repaired project Codex config ${configCleanup.configPath} (backup: ${configCleanup.backupPath})`);
156
+ }
157
+ for (const artifact of projectCleanup.artifacts) {
158
+ log(`Found project-local legacy artifact ${artifact.path}; left in place`);
159
+ }
126
160
 
127
161
  for (const plugin of installed) {
128
162
  log(`Installed ${plugin.name}@${marketplace.name} -> ${plugin.path}`);
129
163
  }
130
164
 
131
- return { marketplaceName: marketplace.name, installed };
165
+ return { marketplaceName: marketplace.name, installed, gitBashPath: gitBashResolution.path, projectCleanup };
166
+ }
167
+
168
+ async function repairProjectLocalCodexArtifactsBestEffort({ startDirectory, codexHome, log }) {
169
+ try {
170
+ return await repairNearestProjectLocalCodexArtifacts({ startDirectory, codexHome });
171
+ } catch (error) {
172
+ log(`Skipped project-local Codex cleanup: ${formatUnknownError(error)}`);
173
+ return emptyProjectLocalCodexCleanupResult();
174
+ }
175
+ }
176
+
177
+ function formatUnknownError(error) {
178
+ return error instanceof Error ? error.message : String(error);
132
179
  }
133
180
 
134
181
  function agentNameFromToml(fileName) {
@@ -170,21 +217,57 @@ function nonEmptyEnvValue(env, key) {
170
217
  const value = env[key];
171
218
  if (typeof value !== "string") return undefined;
172
219
  const trimmed = value.trim();
173
- return trimmed.length === 0 ? undefined : value;
220
+ return trimmed.length === 0 ? undefined : trimmed;
174
221
  }
175
222
 
176
223
  function legacyCacheMarketplaces(marketplaceName) {
177
224
  return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : [];
178
225
  }
179
226
 
227
+ export function resolveDefaultRepoRoot() {
228
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
229
+ }
230
+
180
231
  async function main() {
181
- const repoRoot = process.argv[2] ? resolve(process.argv[2]) : process.cwd();
182
- const result = await installMarketplaceLocally({ repoRoot });
232
+ const parsed = parseLazyCodexInstallCliArgs(process.argv.slice(2));
233
+ if (parsed.kind === "help") {
234
+ console.log(formatLazyCodexInstallHelp());
235
+ return;
236
+ }
237
+ if (parsed.kind === "version") {
238
+ const packageJson = JSON.parse(await readFile(join(resolveDefaultRepoRoot(), "package.json"), "utf8"));
239
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
240
+ console.log(`lazycodex-ai ${version}`);
241
+ return;
242
+ }
243
+ if (parsed.kind === "command") {
244
+ await runDelegatedOmoCommand(parsed, { cwd: process.cwd(), log: console.log, runCommand: defaultRunCommand });
245
+ return;
246
+ }
247
+
248
+ const repoRoot = parsed.repoRoot ? resolve(parsed.repoRoot) : resolveDefaultRepoRoot();
249
+ const result = await installMarketplaceLocally({
250
+ repoRoot,
251
+ autonomousPermissions: parsed.autonomousPermissions,
252
+ });
183
253
  console.log(`Installed ${result.installed.length} plugin(s) from ${result.marketplaceName}.`);
184
254
  }
185
255
 
186
- const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
187
- if (invokedPath === fileURLToPath(import.meta.url)) {
256
+ function resolveEntrypointPath(path) {
257
+ try {
258
+ return realpathSync(resolve(path));
259
+ } catch (error) {
260
+ if (error instanceof Error) return resolve(path);
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ function isEntrypointInvocation(invokedPath) {
266
+ if (!invokedPath) return false;
267
+ return resolveEntrypointPath(invokedPath) === resolveEntrypointPath(fileURLToPath(import.meta.url));
268
+ }
269
+
270
+ if (isEntrypointInvocation(process.argv[1] ?? "")) {
188
271
  main().catch((error) => {
189
272
  console.error(error instanceof Error ? error.message : error);
190
273
  process.exitCode = 1;
@@ -67,6 +67,21 @@ test("#given explicit CODEX_LOCAL_BIN_DIR #when resolving local installer bin di
67
67
  );
68
68
  });
69
69
 
70
+ test("#given CODEX_LOCAL_BIN_DIR with surrounding whitespace #when resolving local installer bin dir #then trims the env value before use", () => {
71
+ const homeDir = join(tmpdir(), "omo-codex-home-trim");
72
+ const codexHome = join(tmpdir(), "omo-codex-install-trim");
73
+ const explicitBinDir = join(tmpdir(), "omo-codex-trim-bin");
74
+
75
+ assert.equal(
76
+ resolveCodexInstallerBinDir({
77
+ codexHome,
78
+ env: { CODEX_LOCAL_BIN_DIR: ` ${explicitBinDir} ` },
79
+ homeDir,
80
+ }),
81
+ explicitBinDir,
82
+ );
83
+ });
84
+
70
85
  test("#given omo plugin source #when inspecting identity #then uses sisyphuslabs omo metadata", async () => {
71
86
  const pluginRoot = join(scriptDir, "..", "plugin");
72
87
 
@@ -12,6 +12,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
12
12
  const codexHome = await makeTempDir();
13
13
  const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
14
14
  const astGrepPackageRoot = join(repoRoot, "packages", "ast-grep-mcp");
15
+ const gitBashPackageRoot = join(repoRoot, "packages", "git-bash-mcp");
15
16
  const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
16
17
 
17
18
  await writeJson(join(astGrepPackageRoot, "package.json"), {
@@ -26,6 +27,12 @@ test("#given external MCP package runtime #when installing cached plugin #then r
26
27
  type: "module",
27
28
  bin: { "omo-lsp": "./dist/cli.js" },
28
29
  });
30
+ await writeJson(join(gitBashPackageRoot, "package.json"), {
31
+ name: "@example/git-bash-mcp",
32
+ version: "0.1.0",
33
+ type: "module",
34
+ bin: { "omo-git-bash": "./dist/cli.js" },
35
+ });
29
36
  await writeJson(join(sourceRoot, "package.json"), {
30
37
  name: "@example/omo",
31
38
  version: "0.1.0",
@@ -37,6 +44,11 @@ test("#given external MCP package runtime #when installing cached plugin #then r
37
44
  args: ["../../ast-grep-mcp/dist/cli.js", "mcp"],
38
45
  cwd: ".",
39
46
  },
47
+ git_bash: {
48
+ command: "node",
49
+ args: ["../../git-bash-mcp/dist/cli.js", "mcp"],
50
+ cwd: ".",
51
+ },
40
52
  lsp: {
41
53
  command: "node",
42
54
  args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
@@ -45,6 +57,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
45
57
  },
46
58
  });
47
59
  await writeJson(join(astGrepPackageRoot, "dist", "cli.js"), { executable: true });
60
+ await writeJson(join(gitBashPackageRoot, "dist", "cli.js"), { executable: true });
48
61
  await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
49
62
  await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
50
63
 
@@ -59,13 +72,17 @@ test("#given external MCP package runtime #when installing cached plugin #then r
59
72
 
60
73
  const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
61
74
  const copiedAstGrepCli = join(result.path, "mcp", "ast_grep", "dist", "cli.js");
75
+ const copiedGitBashCli = join(result.path, "mcp", "git_bash", "dist", "cli.js");
62
76
  const copiedCli = join(result.path, "mcp", "lsp", "dist", "cli.js");
63
77
 
64
78
  assert.deepEqual(cachedMcp.mcpServers.ast_grep.args, [copiedAstGrepCli, "mcp"]);
79
+ assert.deepEqual(cachedMcp.mcpServers.git_bash.args, [copiedGitBashCli, "mcp"]);
65
80
  assert.deepEqual(cachedMcp.mcpServers.lsp.args, [copiedCli, "mcp"]);
66
81
  assert.equal(Object.hasOwn(cachedMcp.mcpServers.ast_grep, "cwd"), false);
82
+ assert.equal(Object.hasOwn(cachedMcp.mcpServers.git_bash, "cwd"), false);
67
83
  assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
68
84
  assert.equal((await stat(copiedAstGrepCli)).isFile(), true);
85
+ assert.equal((await stat(copiedGitBashCli)).isFile(), true);
69
86
  assert.equal((await stat(copiedCli)).isFile(), true);
70
87
  assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
71
88
  });
@@ -171,3 +188,46 @@ test("#given structurally valid external MCP package without mcp suffix #when in
171
188
  assert.deepEqual(cachedMcp.mcpServers.language_tools.args, [copiedCli, "mcp", join(sourceRoot, "..", "local-config.json")]);
172
189
  assert.equal((await stat(copiedCli)).isFile(), true);
173
190
  });
191
+
192
+ test("#given packaged external MCP runtime has only dist files #when installing cached plugin #then runtime is copied into the plugin cache", async () => {
193
+ // given
194
+ const repoRoot = await makeTempDir();
195
+ const codexHome = await makeTempDir();
196
+ const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
197
+ const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
198
+
199
+ await writeJson(join(sourceRoot, "package.json"), {
200
+ name: "@example/omo",
201
+ version: "0.1.0",
202
+ });
203
+ await writeJson(join(sourceRoot, ".mcp.json"), {
204
+ mcpServers: {
205
+ lsp: {
206
+ command: "node",
207
+ args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
208
+ cwd: ".",
209
+ },
210
+ },
211
+ });
212
+ await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
213
+ await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
214
+
215
+ // when
216
+ const result = await installCachedPlugin({
217
+ codexHome,
218
+ marketplaceName: "sisyphuslabs",
219
+ name: "omo",
220
+ runCommand: async () => {},
221
+ sourcePath: sourceRoot,
222
+ version: "0.1.0",
223
+ });
224
+
225
+ // then
226
+ const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
227
+ const copiedCli = join(result.path, "mcp", "lsp", "dist", "cli.js");
228
+ assert.deepEqual(cachedMcp.mcpServers.lsp.args, [copiedCli, "mcp"]);
229
+ assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
230
+ assert.equal((await stat(copiedCli)).isFile(), true);
231
+ assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
232
+ assert.notEqual(cachedMcp.mcpServers.lsp.args[0], join(lspPackageRoot, "dist", "cli.js"));
233
+ });
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { installMarketplaceLocally } from "./install-local.mjs";
7
+ import { makeTempDir, writeJson, writePluginAt } from "./install-test-fixtures.mjs";
8
+
9
+ test("#given packaged lazycodex adapter #when installing locally #then uses bundled artifacts without source builds", async () => {
10
+ // given
11
+ const repoRoot = await makeTempDir();
12
+ const codexHome = await makeTempDir();
13
+ const binDir = await makeTempDir();
14
+ const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
15
+ const pluginRoot = join(codexPackageRoot, "plugin");
16
+ const lspRuntimeRoot = join(repoRoot, "packages", "lsp-tools-mcp");
17
+
18
+ await writeJson(join(repoRoot, "package.json"), {
19
+ name: "lazycodex-ai",
20
+ version: "0.1.2",
21
+ });
22
+ await writeJson(join(codexPackageRoot, "marketplace.json"), {
23
+ name: "sisyphuslabs",
24
+ plugins: [{ name: "omo", source: "./plugins/omo" }],
25
+ });
26
+ await writePluginAt(pluginRoot, "omo", "0.1.0");
27
+ await writeJson(join(pluginRoot, ".mcp.json"), {
28
+ mcpServers: {
29
+ lsp: {
30
+ command: "node",
31
+ args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
32
+ cwd: ".",
33
+ },
34
+ },
35
+ });
36
+ await mkdir(join(pluginRoot, "dist"), { recursive: true });
37
+ await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\nconsole.log('prebuilt')\n");
38
+ await writeJson(join(lspRuntimeRoot, "dist", "cli.js"), { executable: true });
39
+
40
+ const commands = [];
41
+
42
+ // when
43
+ const result = await installMarketplaceLocally({
44
+ repoRoot,
45
+ codexHome,
46
+ binDir,
47
+ platform: "linux",
48
+ runCommand: async (command, args, options) => {
49
+ commands.push([command, args.join(" "), options.cwd]);
50
+ },
51
+ log: () => {},
52
+ });
53
+
54
+ // then
55
+ const pluginCacheRoot = join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0");
56
+ const cachedMcp = JSON.parse(await readFile(join(pluginCacheRoot, ".mcp.json"), "utf8"));
57
+ const cachedLspCli = join(pluginCacheRoot, "mcp", "lsp", "dist", "cli.js");
58
+
59
+ assert.deepEqual(result.installed.map((plugin) => `${plugin.name}@${plugin.version}`), ["omo@0.1.0"]);
60
+ assert.deepEqual(
61
+ commands.map(([command, args, cwd]) => [command, args, cwd]),
62
+ [["npm", "install --omit=dev", pluginCacheRoot]],
63
+ );
64
+ assert.deepEqual(cachedMcp.mcpServers.lsp.args, [cachedLspCli, "mcp"]);
65
+ assert.equal((await stat(cachedLspCli)).isFile(), true);
66
+ assert.notEqual(cachedMcp.mcpServers.lsp.args[0], join(lspRuntimeRoot, "dist", "cli.js"));
67
+ });