oh-my-opencode 4.5.12 → 4.7.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 (189) 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/bin/version-mismatch.js +47 -0
  28. package/bin/version-mismatch.test.ts +120 -0
  29. package/dist/cli/cleanup-command.d.ts +4 -0
  30. package/dist/cli/cleanup.d.ts +11 -0
  31. package/dist/cli/cli-program.d.ts +2 -1
  32. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  33. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  34. package/dist/cli/index.js +2189 -529
  35. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  37. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  38. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  39. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
  40. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  41. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  42. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  43. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  44. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  45. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  46. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  47. package/dist/cli/install-codex/index.d.ts +4 -0
  48. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  49. package/dist/cli/install-codex/types.d.ts +20 -0
  50. package/dist/cli/run/event-state.d.ts +1 -0
  51. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  52. package/dist/cli/run/prompt-start.d.ts +7 -0
  53. package/dist/cli/star-request.d.ts +9 -0
  54. package/dist/config/schema/hooks.d.ts +0 -1
  55. package/dist/create-hooks.d.ts +0 -1
  56. package/dist/features/background-agent/concurrency.d.ts +1 -0
  57. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  58. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  59. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  60. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  61. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  62. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  63. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  64. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  65. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  66. package/dist/hooks/index.d.ts +0 -1
  67. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  68. package/dist/index.js +1077 -563
  69. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  70. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  71. package/dist/plugin/messages-transform.d.ts +8 -1
  72. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  73. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  74. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  75. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  76. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  77. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  78. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  79. package/dist/shared/prompt-async-gate.d.ts +1 -1
  80. package/dist/tools/skill/description-formatter.d.ts +5 -1
  81. package/dist/tools/skill/types.d.ts +1 -0
  82. package/package.json +22 -18
  83. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  84. package/packages/git-bash-mcp/dist/cli.js +367 -0
  85. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  86. package/packages/omo-codex/plugin/.mcp.json +11 -0
  87. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  88. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  89. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  90. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  91. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  92. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  93. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  94. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  95. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  96. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  97. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  98. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  99. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  100. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  101. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  102. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  103. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  104. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  105. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  106. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  107. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  108. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  109. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
  110. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  111. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  112. package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
  113. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
  114. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  115. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
  116. package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
  117. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  118. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  120. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
  121. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  125. package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
  126. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  127. package/packages/omo-codex/plugin/package-lock.json +19 -0
  128. package/packages/omo-codex/plugin/package.json +3 -1
  129. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  130. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  131. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  132. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  133. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
  134. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  135. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  136. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  137. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  138. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  139. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  140. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
  141. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
  142. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
  143. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
  144. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  145. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
  146. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  147. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
  148. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  149. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  150. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  151. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  152. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  153. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
  154. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
  155. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  156. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  157. package/packages/omo-codex/scripts/install/config.mjs +23 -1
  158. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  159. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  160. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  161. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  162. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  163. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  164. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  165. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  166. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  167. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  168. package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
  169. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  170. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  171. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  172. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  173. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  174. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
  175. package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
  176. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
  177. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  178. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  179. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  180. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  181. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  182. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  183. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  184. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  185. package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
  186. package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
  187. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  188. package/postinstall.mjs +36 -3
  189. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,229 @@
1
+ import { copyFile, lstat, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ import { escapeRegExp, findTomlSection, removeSetting } from "./toml-editor.mjs";
5
+
6
+ const LEGACY_AGENT_CONFLICT_KEYS = ["max_threads"];
7
+ const PROJECT_LOCAL_ARTIFACT_PATHS = [
8
+ ".omx",
9
+ ".codex/hooks.json",
10
+ ".codex/agents",
11
+ ".codex/prompts",
12
+ ".codex/skills",
13
+ ];
14
+
15
+ export async function repairNearestProjectLocalCodexArtifacts({ startDirectory, codexHome, now = () => new Date() }) {
16
+ const project = await findProjectLocalCodexConfigs(startDirectory, codexHome);
17
+ if (project === null) {
18
+ return emptyProjectLocalCodexCleanupResult();
19
+ }
20
+
21
+ const artifacts = await collectProjectLocalArtifacts(project.artifactRoots);
22
+ const configs = [];
23
+ for (const configPath of project.configPaths) {
24
+ const original = await readFile(configPath, "utf8");
25
+ const repair = repairProjectLocalCodexConfigText(original);
26
+ if (!repair.changed) {
27
+ configs.push({
28
+ projectRoot: project.projectRoot,
29
+ configPath,
30
+ changed: false,
31
+ removedKeys: repair.removedKeys,
32
+ });
33
+ continue;
34
+ }
35
+
36
+ const backupPath = `${configPath}.backup-${formatBackupTimestamp(now())}`;
37
+ await copyFile(configPath, backupPath);
38
+ await writeFile(configPath, `${repair.config.trimEnd()}\n`);
39
+ configs.push({
40
+ projectRoot: project.projectRoot,
41
+ configPath,
42
+ changed: true,
43
+ removedKeys: repair.removedKeys,
44
+ backupPath,
45
+ });
46
+ }
47
+
48
+ const changedConfigs = configs.filter((config) => config.changed);
49
+ const nearestChangedConfig = lastValue(changedConfigs);
50
+ const nearestConfig = lastValue(configs);
51
+ return {
52
+ projectRoot: project.projectRoot,
53
+ configPath: nearestChangedConfig?.configPath ?? nearestConfig?.configPath ?? null,
54
+ changed: changedConfigs.length > 0,
55
+ removedKeys: uniqueRemovedKeys(changedConfigs),
56
+ backupPath: nearestChangedConfig?.backupPath,
57
+ configs,
58
+ artifacts,
59
+ };
60
+ }
61
+
62
+ export function emptyProjectLocalCodexCleanupResult() {
63
+ return {
64
+ projectRoot: null,
65
+ configPath: null,
66
+ changed: false,
67
+ removedKeys: [],
68
+ configs: [],
69
+ artifacts: [],
70
+ };
71
+ }
72
+
73
+ function uniqueRemovedKeys(configs) {
74
+ const keys = [];
75
+ for (const config of configs) {
76
+ for (const key of config.removedKeys) {
77
+ if (!keys.includes(key)) keys.push(key);
78
+ }
79
+ }
80
+ return keys;
81
+ }
82
+
83
+ function lastValue(values) {
84
+ return values.length > 0 ? (values[values.length - 1] ?? null) : null;
85
+ }
86
+
87
+ export function repairProjectLocalCodexConfigText(config) {
88
+ if (!isMultiAgentV2Enabled(config)) return { config, changed: false, removedKeys: [] };
89
+
90
+ let nextConfig = config;
91
+ const removedKeys = [];
92
+ for (const key of LEGACY_AGENT_CONFLICT_KEYS) {
93
+ const section = findTomlSection(nextConfig, "agents");
94
+ if (section === null || !hasSetting(section.text, key)) continue;
95
+ nextConfig = removeSetting(nextConfig, section, key);
96
+ removedKeys.push(key);
97
+ }
98
+
99
+ return {
100
+ config: nextConfig,
101
+ changed: removedKeys.length > 0,
102
+ removedKeys,
103
+ };
104
+ }
105
+
106
+ async function findProjectLocalCodexConfigs(startDirectory, codexHome) {
107
+ if (typeof startDirectory !== "string" || startDirectory.includes("\0")) return null;
108
+
109
+ const startDirectoryStat = await maybeLstat(startDirectory);
110
+ if (startDirectoryStat !== null && !startDirectoryStat.isDirectory()) {
111
+ throw new ProjectLocalCleanupStartDirectoryError(startDirectory);
112
+ }
113
+
114
+ const codexHomeConfigPath = codexHome === undefined ? null : join(resolve(codexHome), "config.toml");
115
+ let current = resolve(startDirectory);
116
+ const configPathsFromCwd = [];
117
+ while (true) {
118
+ const configPath = join(current, ".codex", "config.toml");
119
+ if (await isRegularProjectLocalConfig(current, configPath)) {
120
+ if (codexHomeConfigPath === null || resolve(configPath) !== codexHomeConfigPath) {
121
+ configPathsFromCwd.push(configPath);
122
+ }
123
+ }
124
+
125
+ if (await exists(join(current, ".git"))) {
126
+ return configPathsFromCwd.length === 0
127
+ ? null
128
+ : {
129
+ projectRoot: current,
130
+ configPaths: [...configPathsFromCwd].reverse(),
131
+ artifactRoots: artifactRootsForConfigPaths(configPathsFromCwd),
132
+ };
133
+ }
134
+
135
+ const parent = dirname(current);
136
+ if (parent === current) {
137
+ const nearestConfigPath = configPathsFromCwd[0];
138
+ return nearestConfigPath === undefined
139
+ ? null
140
+ : {
141
+ projectRoot: dirname(dirname(nearestConfigPath)),
142
+ configPaths: [nearestConfigPath],
143
+ artifactRoots: [dirname(dirname(nearestConfigPath))],
144
+ };
145
+ }
146
+ current = parent;
147
+ }
148
+ }
149
+
150
+ async function isRegularProjectLocalConfig(directory, configPath) {
151
+ const codexDirStat = await maybeLstat(join(directory, ".codex"));
152
+ if (codexDirStat === null || !codexDirStat.isDirectory() || codexDirStat.isSymbolicLink()) return false;
153
+ const configStat = await maybeLstat(configPath);
154
+ return configStat !== null && configStat.isFile() && !configStat.isSymbolicLink();
155
+ }
156
+
157
+ function artifactRootsForConfigPaths(configPaths) {
158
+ const roots = [];
159
+ for (const configPath of configPaths) {
160
+ const root = dirname(dirname(configPath));
161
+ if (!roots.includes(root)) roots.push(root);
162
+ }
163
+ return roots.reverse();
164
+ }
165
+
166
+ async function collectProjectLocalArtifacts(projectRoots) {
167
+ const artifacts = [];
168
+ const seenPaths = new Set();
169
+ for (const projectRoot of projectRoots) {
170
+ for (const relativePath of PROJECT_LOCAL_ARTIFACT_PATHS) {
171
+ const artifactPath = join(projectRoot, relativePath);
172
+ if (seenPaths.has(artifactPath)) continue;
173
+ const entryStat = await maybeLstat(artifactPath);
174
+ if (entryStat === null) continue;
175
+ seenPaths.add(artifactPath);
176
+ artifacts.push({
177
+ relativePath,
178
+ path: artifactPath,
179
+ kind: entryStat.isDirectory() ? "directory" : entryStat.isFile() ? "file" : "other",
180
+ });
181
+ }
182
+ }
183
+ return artifacts;
184
+ }
185
+
186
+ function isMultiAgentV2Enabled(config) {
187
+ const featuresSection = findTomlSection(config, "features");
188
+ if (featuresSection !== null && settingIsBooleanTrue(featuresSection.text, "multi_agent_v2")) return true;
189
+
190
+ const multiAgentSection = findTomlSection(config, "features.multi_agent_v2");
191
+ return multiAgentSection !== null && settingIsBooleanTrue(multiAgentSection.text, "enabled");
192
+ }
193
+
194
+ function settingIsBooleanTrue(sectionText, key) {
195
+ return new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*true\\s*(?:#.*)?$`, "m").test(sectionText);
196
+ }
197
+
198
+ function hasSetting(sectionText, key) {
199
+ return new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`, "m").test(sectionText);
200
+ }
201
+
202
+ function formatBackupTimestamp(date) {
203
+ return date.toISOString().replace(/[:.]/g, "-");
204
+ }
205
+
206
+ async function maybeLstat(path) {
207
+ try {
208
+ return await lstat(path);
209
+ } catch (error) {
210
+ if (nodeErrorCode(error) === "ENOENT") return null;
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async function exists(path) {
216
+ return (await maybeLstat(path)) !== null;
217
+ }
218
+
219
+ function nodeErrorCode(error) {
220
+ if (!(error instanceof Error) || !("code" in error)) return null;
221
+ return typeof error.code === "string" ? error.code : null;
222
+ }
223
+
224
+ class ProjectLocalCleanupStartDirectoryError extends Error {
225
+ constructor(startDirectory) {
226
+ super(`Project-local Codex cleanup start path is not a directory: ${startDirectory}`);
227
+ this.name = "ProjectLocalCleanupStartDirectoryError";
228
+ }
229
+ }
@@ -0,0 +1,72 @@
1
+ import { replaceOrInsertRootSetting } from "./toml-editor.mjs";
2
+
3
+ const MANAGED_KEYS = ["model", "model_context_window", "model_reasoning_effort", "plan_mode_reasoning_effort"];
4
+
5
+ export function ensureCodexReasoningConfig(config, catalog) {
6
+ const current = readRootReasoningSettings(config);
7
+ if (
8
+ Object.keys(current).length > 0 &&
9
+ !matchesProfile(current, catalog.current) &&
10
+ !catalog.managedProfiles.some((profile) => matchesProfile(current, profile))
11
+ ) {
12
+ return config;
13
+ }
14
+ let next = replaceOrInsertRootSetting(config, "model", JSON.stringify(catalog.current.model));
15
+ next = replaceOrInsertRootSetting(next, "model_context_window", catalog.current.modelContextWindow.toString());
16
+ next = replaceOrInsertRootSetting(
17
+ next,
18
+ "model_reasoning_effort",
19
+ JSON.stringify(catalog.current.modelReasoningEffort),
20
+ );
21
+ next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(catalog.current.planModeReasoningEffort));
22
+ return next;
23
+ }
24
+
25
+ function readRootReasoningSettings(config) {
26
+ const settings = {};
27
+ for (const line of config.split(/\n/)) {
28
+ if (isSectionHeader(line)) break;
29
+ for (const key of MANAGED_KEYS) {
30
+ if (!isRootSetting(line, key)) continue;
31
+ const value = parseTomlScalar(line.slice(line.indexOf("=") + 1));
32
+ if (key === "model" && typeof value === "string") settings.model = value;
33
+ if (key === "model_context_window" && typeof value === "number") settings.modelContextWindow = value;
34
+ if (key === "model_reasoning_effort" && typeof value === "string") settings.modelReasoningEffort = value;
35
+ if (key === "plan_mode_reasoning_effort" && typeof value === "string") settings.planModeReasoningEffort = value;
36
+ }
37
+ }
38
+ return settings;
39
+ }
40
+
41
+ function matchesProfile(current, profile) {
42
+ for (const [key, value] of Object.entries(profile)) {
43
+ if (current[key] !== value) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function parseTomlScalar(value) {
49
+ const trimmed = value.trim();
50
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
51
+ try {
52
+ return JSON.parse(trimmed);
53
+ } catch (error) {
54
+ if (error instanceof SyntaxError) return undefined;
55
+ throw error;
56
+ }
57
+ }
58
+ const numeric = Number(trimmed);
59
+ return Number.isFinite(numeric) ? numeric : undefined;
60
+ }
61
+
62
+ function isSectionHeader(line) {
63
+ const trimmed = line.trim();
64
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
65
+ }
66
+
67
+ function isRootSetting(line, key) {
68
+ const trimmed = line.trimStart();
69
+ if (trimmed.startsWith("#") || trimmed.startsWith("[")) return false;
70
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
71
+ return match?.[1] === key;
72
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ const PACKAGED_INSTALLER_PACKAGE_NAMES = new Set([
6
+ "@code-yeongyu/lazycodex",
7
+ "@code-yeongyu/lazycodex-ai",
8
+ "lazycodex",
9
+ "lazycodex-ai",
10
+ "oh-my-opencode",
11
+ "oh-my-openagent",
12
+ ]);
13
+
14
+ export async function shouldBuildSourcePackages(repoRoot) {
15
+ if (existsSync(join(repoRoot, "src", "index.ts"))) return true;
16
+ const packageJsonPath = join(repoRoot, "package.json");
17
+ if (!existsSync(packageJsonPath)) return true;
18
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
19
+ return !PACKAGED_INSTALLER_PACKAGE_NAMES.has(packageJson?.name);
20
+ }
@@ -26,22 +26,39 @@ export function replaceOrInsertSetting(config, section, key, value) {
26
26
  }
27
27
 
28
28
  export function removeSetting(config, section, key) {
29
- const linePattern = new RegExp(`^${escapeRegExp(key)}\\s*=.*(?:\\n|$)`, "m");
29
+ const linePattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=.*(?:\\n|$)`, "m");
30
30
  const replacement = section.text.replace(linePattern, "");
31
31
  return config.slice(0, section.start) + replacement + config.slice(section.end);
32
32
  }
33
33
 
34
+ export function replaceOrInsertRootSetting(config, key, value) {
35
+ const sectionStart = findFirstTableStart(config);
36
+ const root = config.slice(0, sectionStart);
37
+ const suffix = config.slice(sectionStart);
38
+ const linePattern = new RegExp(`^${escapeRegExp(key)}\\s*=.*$`, "m");
39
+ const replacement = linePattern.test(root)
40
+ ? root.replace(linePattern, `${key} = ${value}`)
41
+ : `${root.trimEnd()}${root.trimEnd().length > 0 ? "\n" : ""}${key} = ${value}\n`;
42
+ if (suffix.length === 0) return replacement;
43
+ return `${replacement.trimEnd()}\n\n${suffix.trimStart()}`;
44
+ }
45
+
34
46
  export function appendBlock(config, block) {
35
47
  const prefix = config.trimEnd();
36
48
  return `${prefix}${prefix.length > 0 ? "\n\n" : ""}${block.trimEnd()}\n`;
37
49
  }
38
50
 
51
+ function findFirstTableStart(config) {
52
+ const match = config.match(/^[[].*$/m);
53
+ return match?.index ?? config.length;
54
+ }
55
+
39
56
  function insertSetting(sectionText, key, value) {
40
57
  const lines = sectionText.split("\n");
41
58
  lines.splice(1, 0, `${key} = ${value}`);
42
59
  return lines.join("\n");
43
60
  }
44
61
 
45
- function escapeRegExp(value) {
62
+ export function escapeRegExp(value) {
46
63
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
64
  }
@@ -76,6 +76,29 @@ test("#given managed legacy Codex component symlink #when linking bins #then rem
76
76
  assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
77
77
  });
78
78
 
79
+ test("#given managed legacy Codex LSP symlink #when linking bins #then removes stale lsp symlink", async () => {
80
+ const root = await makeTempDir();
81
+ const pluginRoot = join(root, "plugin");
82
+ const binDir = join(root, "bin");
83
+ const oldTarget = join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist", "cli.js");
84
+
85
+ await mkdir(join(pluginRoot, "dist"), { recursive: true });
86
+ await mkdir(join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist"), { recursive: true });
87
+ await mkdir(binDir, { recursive: true });
88
+ await writeJson(join(pluginRoot, "package.json"), {
89
+ name: "@example/omo",
90
+ bin: { omo: "./dist/cli.js" },
91
+ });
92
+ await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
93
+ await writeFile(oldTarget, "#!/usr/bin/env node\n");
94
+ await symlink(oldTarget, join(binDir, "codex-lsp"));
95
+
96
+ await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
97
+
98
+ await assert.rejects(readlink(join(binDir, "codex-lsp")));
99
+ assert.equal(await readlink(join(binDir, "omo")), join(pluginRoot, "dist", "cli.js"));
100
+ });
101
+
79
102
  test("#given user-owned legacy Codex symlink #when linking bins #then preserves the user symlink", async () => {
80
103
  const root = await makeTempDir();
81
104
  const pluginRoot = join(root, "plugin");
@@ -0,0 +1,146 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseLazyCodexInstallCliArgs } from "./install/cli-args.mjs";
5
+
6
+ test("#given lazycodex install flags #when parsing Node installer argv #then keeps Codex autonomous intent", () => {
7
+ // given
8
+ const argv = ["install", "--no-tui", "--codex-autonomous", "--platform=codex"];
9
+
10
+ // when
11
+ const parsed = parseLazyCodexInstallCliArgs(argv);
12
+
13
+ // then
14
+ assert.deepEqual(parsed, {
15
+ kind: "install",
16
+ autonomousPermissions: true,
17
+ repoRoot: undefined,
18
+ });
19
+ });
20
+
21
+ test("#given unsupported OpenCode platform override #when parsing Node installer argv #then rejects the Bun-backed path", () => {
22
+ // given
23
+ const argv = ["install", "--platform=both"];
24
+
25
+ // when
26
+ const parse = () => parseLazyCodexInstallCliArgs(argv);
27
+
28
+ // then
29
+ assert.throws(parse, /lazycodex-ai installs the Codex Light edition only/);
30
+ });
31
+
32
+ test("#given missing platform value #when parsing Node installer argv #then rejects the incomplete option", () => {
33
+ // given
34
+ const argv = ["install", "--platform"];
35
+
36
+ // when
37
+ const parse = () => parseLazyCodexInstallCliArgs(argv);
38
+
39
+ // then
40
+ assert.throws(parse, /--platform requires a value/);
41
+ });
42
+
43
+ test("#given repo root equals option #when parsing Node installer argv #then keeps the explicit path", () => {
44
+ // given
45
+ const argv = ["install", "--repo-root=/tmp/project"];
46
+
47
+ // when
48
+ const parsed = parseLazyCodexInstallCliArgs(argv);
49
+
50
+ // then
51
+ assert.deepEqual(parsed, {
52
+ kind: "install",
53
+ autonomousPermissions: undefined,
54
+ repoRoot: "/tmp/project",
55
+ });
56
+ });
57
+
58
+ test("#given unknown positional command #when parsing Node installer argv #then rejects instead of treating it as a repo root", () => {
59
+ // given
60
+ const argv = ["banana"];
61
+
62
+ // when
63
+ const parse = () => parseLazyCodexInstallCliArgs(argv);
64
+
65
+ // then
66
+ assert.throws(parse, /Unsupported lazycodex-ai command: banana/);
67
+ });
68
+
69
+ test("#given install help flag #when parsing Node installer argv #then returns help", () => {
70
+ // given
71
+ const argv = ["install", "--help"];
72
+
73
+ // when
74
+ const parsed = parseLazyCodexInstallCliArgs(argv);
75
+
76
+ // then
77
+ assert.deepEqual(parsed, { kind: "help" });
78
+ });
79
+
80
+ test("#given dry-run install with codex autonomy flags #when parsing Node installer argv #then keeps delegated install command and dry-run intent", () => {
81
+ // given
82
+ const argv = ["--dry-run", "install", "--no-tui", "--codex-autonomous"];
83
+
84
+ // when
85
+ const parsed = parseLazyCodexInstallCliArgs(argv);
86
+
87
+ // then
88
+ assert.deepEqual(parsed, {
89
+ kind: "command",
90
+ command: "install",
91
+ dryRun: true,
92
+ noTui: true,
93
+ skipAuth: false,
94
+ autonomousPermissions: true,
95
+ repoRoot: undefined,
96
+ args: [],
97
+ });
98
+ });
99
+
100
+ test("#given dry-run doctor command #when parsing Node installer argv #then returns delegated doctor command", () => {
101
+ // given
102
+ const argv = ["--dry-run", "doctor"];
103
+
104
+ // when
105
+ const parsed = parseLazyCodexInstallCliArgs(argv);
106
+
107
+ // then
108
+ assert.deepEqual(parsed, {
109
+ kind: "command",
110
+ command: "doctor",
111
+ dryRun: true,
112
+ args: [],
113
+ });
114
+ });
115
+
116
+ test("#given dry-run cleanup command #when parsing Node installer argv #then returns delegated codex cleanup command", () => {
117
+ // given
118
+ const argv = ["--dry-run", "cleanup", "--project", "/tmp/lazycodex-qa"];
119
+
120
+ // when
121
+ const parsed = parseLazyCodexInstallCliArgs(argv);
122
+
123
+ // then
124
+ assert.deepEqual(parsed, {
125
+ kind: "command",
126
+ command: "cleanup",
127
+ dryRun: true,
128
+ args: ["--project", "/tmp/lazycodex-qa"],
129
+ });
130
+ });
131
+
132
+ test("#given doctor flags #when parsing Node installer argv #then preserves pass-through arguments", () => {
133
+ // given
134
+ const argv = ["doctor", "--json"];
135
+
136
+ // when
137
+ const parsed = parseLazyCodexInstallCliArgs(argv);
138
+
139
+ // then
140
+ assert.deepEqual(parsed, {
141
+ kind: "command",
142
+ command: "doctor",
143
+ dryRun: false,
144
+ args: ["--json"],
145
+ });
146
+ });
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, 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 { updateCodexConfig } from "./install/config.mjs";
8
+
9
+ test("#given autonomous permissions requested #when script installer updates config #then enables full Codex autonomy", async () => {
10
+ // given
11
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-"));
12
+ const configPath = join(root, "config.toml");
13
+ await writeFile(
14
+ configPath,
15
+ [
16
+ 'approval_policy = "on-request"',
17
+ 'sandbox_mode = "workspace-write"',
18
+ 'network_access = "disabled"',
19
+ "",
20
+ "[notice]",
21
+ "hide_full_access_warning = false",
22
+ "",
23
+ "[windows]",
24
+ 'sandbox = "workspace-write"',
25
+ "",
26
+ ].join("\n"),
27
+ );
28
+
29
+ // when
30
+ await updateCodexConfig({
31
+ configPath,
32
+ repoRoot: "/repo/packages/omo-codex",
33
+ marketplaceName: "debug",
34
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
35
+ pluginNames: ["omo"],
36
+ autonomousPermissions: true,
37
+ });
38
+
39
+ // then
40
+ const config = await readFile(configPath, "utf8");
41
+ assert.match(config, /approval_policy = "never"/);
42
+ assert.match(config, /sandbox_mode = "danger-full-access"/);
43
+ assert.match(config, /network_access = "enabled"/);
44
+ assert.match(config, /\[notice\]/);
45
+ assert.match(config, /hide_full_access_warning = true/);
46
+ assert.match(config, /hide_world_writable_warning = true/);
47
+ assert.doesNotMatch(config, /sandbox = "workspace-write"/);
48
+ });