oh-my-codex 0.8.2 → 0.8.4

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 (53) hide show
  1. package/README.md +10 -2
  2. package/dist/cli/__tests__/setup-agents-overwrite.test.js +17 -27
  3. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  4. package/dist/cli/__tests__/setup-codex-version.test.d.ts +2 -0
  5. package/dist/cli/__tests__/setup-codex-version.test.d.ts.map +1 -0
  6. package/dist/cli/__tests__/setup-codex-version.test.js +99 -0
  7. package/dist/cli/__tests__/setup-codex-version.test.js.map +1 -0
  8. package/dist/cli/__tests__/setup-refresh.test.d.ts +2 -0
  9. package/dist/cli/__tests__/setup-refresh.test.d.ts.map +1 -0
  10. package/dist/cli/__tests__/setup-refresh.test.js +166 -0
  11. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -0
  12. package/dist/cli/__tests__/setup-scope.test.js +7 -4
  13. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  14. package/dist/cli/__tests__/setup-skills-overwrite.test.js +5 -2
  15. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  16. package/dist/cli/__tests__/uninstall.test.js +53 -0
  17. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  18. package/dist/cli/setup.d.ts +2 -1
  19. package/dist/cli/setup.d.ts.map +1 -1
  20. package/dist/cli/setup.js +335 -175
  21. package/dist/cli/setup.js.map +1 -1
  22. package/dist/config/__tests__/generator-idempotent.test.js +199 -128
  23. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  24. package/dist/config/__tests__/generator-notify.test.js +55 -0
  25. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  26. package/dist/config/generator.d.ts +4 -1
  27. package/dist/config/generator.d.ts.map +1 -1
  28. package/dist/config/generator.js +154 -111
  29. package/dist/config/generator.js.map +1 -1
  30. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +26 -4
  31. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  32. package/dist/team/__tests__/runtime.test.js +76 -0
  33. package/dist/team/__tests__/runtime.test.js.map +1 -1
  34. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +4 -3
  35. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
  36. package/dist/team/__tests__/tmux-session.test.js +10 -3
  37. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  38. package/dist/team/runtime.d.ts.map +1 -1
  39. package/dist/team/runtime.js +92 -62
  40. package/dist/team/runtime.js.map +1 -1
  41. package/dist/team/tmux-session.d.ts +4 -3
  42. package/dist/team/tmux-session.d.ts.map +1 -1
  43. package/dist/team/tmux-session.js +15 -9
  44. package/dist/team/tmux-session.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/hooks/__tests__/emulator.test.d.ts +0 -2
  47. package/dist/hooks/__tests__/emulator.test.d.ts.map +0 -1
  48. package/dist/hooks/__tests__/emulator.test.js +0 -53
  49. package/dist/hooks/__tests__/emulator.test.js.map +0 -1
  50. package/dist/hooks/emulator.d.ts +0 -44
  51. package/dist/hooks/emulator.d.ts.map +0 -1
  52. package/dist/hooks/emulator.js +0 -105
  53. package/dist/hooks/emulator.js.map +0 -1
package/dist/cli/setup.js CHANGED
@@ -2,55 +2,120 @@
2
2
  * omx setup - Automated installation of oh-my-codex
3
3
  * Installs skills, prompts, MCP servers config, and AGENTS.md
4
4
  */
5
- import { mkdir, copyFile, readdir, readFile, writeFile, stat, rm } from 'fs/promises';
6
- import { join, dirname } from 'path';
7
- import { existsSync } from 'fs';
8
- import { spawnSync } from 'child_process';
9
- import { createInterface } from 'readline/promises';
10
- import { codexHome, codexConfigPath, codexPromptsDir, userSkillsDir, omxStateDir, omxPlansDir, omxLogsDir, omxAgentsConfigDir, } from '../utils/paths.js';
11
- import { mergeConfig } from '../config/generator.js';
12
- import { installNativeAgentConfigs } from '../agents/native-config.js';
13
- import { getPackageRoot } from '../utils/package.js';
14
- import { readSessionState, isSessionStale } from '../hooks/session.js';
15
- import { getCatalogHeadlineCounts } from './catalog-contract.js';
16
- import { tryReadCatalogManifest } from '../catalog/reader.js';
5
+ import { mkdir, copyFile, readdir, readFile, writeFile, stat, rm, } from "fs/promises";
6
+ import { join, dirname, relative } from "path";
7
+ import { existsSync } from "fs";
8
+ import { spawnSync } from "child_process";
9
+ import { createInterface } from "readline/promises";
10
+ import { homedir } from "os";
11
+ import { codexHome, codexConfigPath, codexPromptsDir, userSkillsDir, omxStateDir, omxPlansDir, omxLogsDir, omxAgentsConfigDir, } from "../utils/paths.js";
12
+ import { buildMergedConfig, getRootModelName } from "../config/generator.js";
13
+ import { generateAgentToml } from "../agents/native-config.js";
14
+ import { AGENT_DEFINITIONS } from "../agents/definitions.js";
15
+ import { getPackageRoot } from "../utils/package.js";
16
+ import { readSessionState, isSessionStale } from "../hooks/session.js";
17
+ import { getCatalogHeadlineCounts } from "./catalog-contract.js";
18
+ import { tryReadCatalogManifest } from "../catalog/reader.js";
17
19
  /**
18
20
  * Legacy scope values that may appear in persisted setup-scope.json files.
19
21
  * Both 'project-local' (renamed) and old 'project' (minimal, removed) are
20
22
  * migrated to the current 'project' scope on read.
21
23
  */
22
24
  const LEGACY_SCOPE_MIGRATION = {
23
- 'project-local': 'project',
25
+ "project-local": "project",
24
26
  };
25
- export const SETUP_SCOPES = ['user', 'project'];
27
+ export const SETUP_SCOPES = ["user", "project"];
26
28
  function applyScopePathRewritesToAgentsTemplate(content, scope) {
27
- if (scope !== 'project')
29
+ if (scope !== "project")
28
30
  return content;
29
31
  return content
30
- .replaceAll('~/.codex', './.codex')
31
- .replaceAll('~/.agents', './.agents');
32
+ .replaceAll("~/.codex", "./.codex")
33
+ .replaceAll("~/.agents", "./.agents");
32
34
  }
33
35
  const REQUIRED_TEAM_CLI_API_MARKERS = [
34
- 'if (subcommand === \'api\')',
35
- 'executeTeamApiOperation',
36
- 'TEAM_API_OPERATIONS',
36
+ "if (subcommand === 'api')",
37
+ "executeTeamApiOperation",
38
+ "TEAM_API_OPERATIONS",
37
39
  ];
38
- const DEFAULT_SETUP_SCOPE = 'user';
40
+ const DEFAULT_SETUP_SCOPE = "user";
41
+ const LEGACY_SETUP_MODEL = "gpt-5.3-codex";
42
+ const DEFAULT_SETUP_MODEL = "gpt-5.4";
43
+ function createEmptyCategorySummary() {
44
+ return {
45
+ updated: 0,
46
+ unchanged: 0,
47
+ backedUp: 0,
48
+ skipped: 0,
49
+ removed: 0,
50
+ };
51
+ }
52
+ function createEmptyRunSummary() {
53
+ return {
54
+ prompts: createEmptyCategorySummary(),
55
+ skills: createEmptyCategorySummary(),
56
+ nativeAgents: createEmptyCategorySummary(),
57
+ agentsMd: createEmptyCategorySummary(),
58
+ config: createEmptyCategorySummary(),
59
+ };
60
+ }
61
+ function getBackupContext(scope, projectRoot) {
62
+ const timestamp = new Date().toISOString().replace(/[:]/g, "-");
63
+ if (scope === "project") {
64
+ return {
65
+ backupRoot: join(projectRoot, ".omx", "backups", "setup", timestamp),
66
+ baseRoot: projectRoot,
67
+ };
68
+ }
69
+ return {
70
+ backupRoot: join(homedir(), ".omx", "backups", "setup", timestamp),
71
+ baseRoot: homedir(),
72
+ };
73
+ }
74
+ async function ensureBackup(destinationPath, contentChanged, backupContext, options) {
75
+ if (!contentChanged || !existsSync(destinationPath))
76
+ return false;
77
+ const relativePath = relative(backupContext.baseRoot, destinationPath);
78
+ const safeRelativePath = relativePath.startsWith("..") || relativePath === ""
79
+ ? destinationPath.replace(/^[/]+/, "")
80
+ : relativePath;
81
+ const backupPath = join(backupContext.backupRoot, safeRelativePath);
82
+ if (!options.dryRun) {
83
+ await mkdir(dirname(backupPath), { recursive: true });
84
+ await copyFile(destinationPath, backupPath);
85
+ }
86
+ if (options.verbose) {
87
+ console.log(` backup ${destinationPath} -> ${backupPath}`);
88
+ }
89
+ return true;
90
+ }
91
+ async function filesDiffer(src, dst) {
92
+ if (!existsSync(dst))
93
+ return true;
94
+ const [srcContent, dstContent] = await Promise.all([
95
+ readFile(src, "utf-8"),
96
+ readFile(dst, "utf-8"),
97
+ ]);
98
+ return srcContent !== dstContent;
99
+ }
100
+ function logCategorySummary(name, summary) {
101
+ console.log(` ${name}: updated=${summary.updated}, unchanged=${summary.unchanged}, ` +
102
+ `backed_up=${summary.backedUp}, skipped=${summary.skipped}, removed=${summary.removed}`);
103
+ }
39
104
  function isSetupScope(value) {
40
105
  return SETUP_SCOPES.includes(value);
41
106
  }
42
107
  function getScopeFilePath(projectRoot) {
43
- return join(projectRoot, '.omx', 'setup-scope.json');
108
+ return join(projectRoot, ".omx", "setup-scope.json");
44
109
  }
45
110
  export function resolveScopeDirectories(scope, projectRoot) {
46
- if (scope === 'project') {
47
- const codexHomeDir = join(projectRoot, '.codex');
111
+ if (scope === "project") {
112
+ const codexHomeDir = join(projectRoot, ".codex");
48
113
  return {
49
- codexConfigFile: join(codexHomeDir, 'config.toml'),
114
+ codexConfigFile: join(codexHomeDir, "config.toml"),
50
115
  codexHomeDir,
51
- nativeAgentsDir: join(projectRoot, '.omx', 'agents'),
52
- promptsDir: join(codexHomeDir, 'prompts'),
53
- skillsDir: join(projectRoot, '.agents', 'skills'),
116
+ nativeAgentsDir: join(projectRoot, ".omx", "agents"),
117
+ promptsDir: join(codexHomeDir, "prompts"),
118
+ skillsDir: join(projectRoot, ".agents", "skills"),
54
119
  };
55
120
  }
56
121
  return {
@@ -66,9 +131,9 @@ async function readPersistedSetupScope(projectRoot) {
66
131
  if (!existsSync(scopePath))
67
132
  return undefined;
68
133
  try {
69
- const raw = await readFile(scopePath, 'utf-8');
134
+ const raw = await readFile(scopePath, "utf-8");
70
135
  const parsed = JSON.parse(raw);
71
- if (parsed && typeof parsed.scope === 'string') {
136
+ if (parsed && typeof parsed.scope === "string") {
72
137
  // Direct match to current scopes
73
138
  if (isSetupScope(parsed.scope))
74
139
  return parsed.scope;
@@ -95,19 +160,21 @@ async function promptForSetupScope(defaultScope) {
95
160
  output: process.stdout,
96
161
  });
97
162
  try {
98
- console.log('Select setup scope:');
163
+ console.log("Select setup scope:");
99
164
  console.log(` 1) user (default) — installs to ~/.codex, ~/.agents`);
100
- console.log(' 2) project — installs to ./.codex, ./.agents (local to project)');
101
- const answer = (await rl.question('Scope [1-2] (default: 1): ')).trim().toLowerCase();
102
- if (answer === '2' || answer === 'project')
103
- return 'project';
165
+ console.log(" 2) project — installs to ./.codex, ./.agents (local to project)");
166
+ const answer = (await rl.question("Scope [1-2] (default: 1): "))
167
+ .trim()
168
+ .toLowerCase();
169
+ if (answer === "2" || answer === "project")
170
+ return "project";
104
171
  return defaultScope;
105
172
  }
106
173
  finally {
107
174
  rl.close();
108
175
  }
109
176
  }
110
- async function promptForAgentsOverwrite() {
177
+ async function promptForModelUpgrade(currentModel, targetModel) {
111
178
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
112
179
  return false;
113
180
  }
@@ -116,10 +183,10 @@ async function promptForAgentsOverwrite() {
116
183
  output: process.stdout,
117
184
  });
118
185
  try {
119
- const answer = (await rl.question('AGENTS.md already exists. Overwrite with template? [y/N]: '))
186
+ const answer = (await rl.question(`Detected model "${currentModel}". Update to "${targetModel}"? [Y/n]: `))
120
187
  .trim()
121
188
  .toLowerCase();
122
- return answer === 'y' || answer === 'yes';
189
+ return answer === "" || answer === "y" || answer === "yes";
123
190
  }
124
191
  finally {
125
192
  rl.close();
@@ -127,17 +194,17 @@ async function promptForAgentsOverwrite() {
127
194
  }
128
195
  async function resolveSetupScope(projectRoot, requestedScope) {
129
196
  if (requestedScope) {
130
- return { scope: requestedScope, source: 'cli' };
197
+ return { scope: requestedScope, source: "cli" };
131
198
  }
132
199
  const persisted = await readPersistedSetupScope(projectRoot);
133
200
  if (persisted) {
134
- return { scope: persisted, source: 'persisted' };
201
+ return { scope: persisted, source: "persisted" };
135
202
  }
136
203
  if (process.stdin.isTTY && process.stdout.isTTY) {
137
204
  const scope = await promptForSetupScope(DEFAULT_SETUP_SCOPE);
138
- return { scope, source: 'prompt' };
205
+ return { scope, source: "prompt" };
139
206
  }
140
- return { scope: DEFAULT_SETUP_SCOPE, source: 'default' };
207
+ return { scope: DEFAULT_SETUP_SCOPE, source: "default" };
141
208
  }
142
209
  async function persistSetupScope(projectRoot, scope, options) {
143
210
  const scopePath = getScopeFilePath(projectRoot);
@@ -148,22 +215,22 @@ async function persistSetupScope(projectRoot, scope, options) {
148
215
  }
149
216
  await mkdir(dirname(scopePath), { recursive: true });
150
217
  const payload = { scope };
151
- await writeFile(scopePath, JSON.stringify(payload, null, 2) + '\n');
218
+ await writeFile(scopePath, JSON.stringify(payload, null, 2) + "\n");
152
219
  if (options.verbose)
153
220
  console.log(` Wrote ${scopePath}`);
154
221
  }
155
222
  export async function setup(options = {}) {
156
- const { force = false, dryRun = false, scope: requestedScope, verbose = false, agentsOverwritePrompt, } = options;
223
+ const { force = false, dryRun = false, scope: requestedScope, verbose = false, modelUpgradePrompt, } = options;
157
224
  const pkgRoot = getPackageRoot();
158
225
  const projectRoot = process.cwd();
159
226
  const resolvedScope = await resolveSetupScope(projectRoot, requestedScope);
160
227
  const scopeDirs = resolveScopeDirectories(resolvedScope.scope, projectRoot);
161
- const scopeSourceMessage = resolvedScope.source === 'persisted' ? ' (from .omx/setup-scope.json)' : '';
162
- console.log('oh-my-codex setup');
163
- console.log('=================\n');
228
+ const scopeSourceMessage = resolvedScope.source === "persisted" ? " (from .omx/setup-scope.json)" : "";
229
+ console.log("oh-my-codex setup");
230
+ console.log("=================\n");
164
231
  console.log(`Using setup scope: ${resolvedScope.scope}${scopeSourceMessage}\n`);
165
232
  // Step 1: Ensure directories exist
166
- console.log('[1/8] Creating directories...');
233
+ console.log("[1/8] Creating directories...");
167
234
  const dirs = [
168
235
  scopeDirs.codexHomeDir,
169
236
  scopeDirs.promptsDir,
@@ -180,19 +247,25 @@ export async function setup(options = {}) {
180
247
  if (verbose)
181
248
  console.log(` mkdir ${dir}`);
182
249
  }
183
- await persistSetupScope(projectRoot, resolvedScope.scope, { dryRun, verbose });
184
- console.log(' Done.\n');
185
- // Step 2: Install agent prompts
186
- console.log('[2/8] Installing agent prompts...');
250
+ await persistSetupScope(projectRoot, resolvedScope.scope, {
251
+ dryRun,
252
+ verbose,
253
+ });
254
+ console.log(" Done.\n");
187
255
  const catalogCounts = getCatalogHeadlineCounts();
256
+ const summary = createEmptyRunSummary();
257
+ const backupContext = getBackupContext(resolvedScope.scope, projectRoot);
258
+ // Step 2: Install agent prompts
259
+ console.log("[2/8] Installing agent prompts...");
188
260
  {
189
- const promptsSrc = join(pkgRoot, 'prompts');
261
+ const promptsSrc = join(pkgRoot, "prompts");
190
262
  const promptsDst = scopeDirs.promptsDir;
191
- const promptCount = await installDirectory(promptsSrc, promptsDst, '.md', { force, dryRun, verbose });
263
+ summary.prompts = await installDirectory(promptsSrc, promptsDst, ".md", backupContext, { force, dryRun, verbose }, "prompt");
192
264
  const cleanedLegacyPromptShims = await cleanupLegacySkillPromptShims(promptsSrc, promptsDst, {
193
265
  dryRun,
194
266
  verbose,
195
267
  });
268
+ summary.prompts.removed += cleanedLegacyPromptShims;
196
269
  if (cleanedLegacyPromptShims > 0) {
197
270
  if (dryRun) {
198
271
  console.log(` Would remove ${cleanedLegacyPromptShims} legacy skill prompt shim file(s).`);
@@ -202,127 +275,133 @@ export async function setup(options = {}) {
202
275
  }
203
276
  }
204
277
  if (catalogCounts) {
205
- console.log(` Installed ${promptCount} agent prompts (catalog baseline: ${catalogCounts.prompts}).\n`);
278
+ console.log(` Prompt refresh complete (catalog baseline: ${catalogCounts.prompts}).\n`);
206
279
  }
207
280
  else {
208
- console.log(` Installed ${promptCount} agent prompts.\n`);
281
+ console.log(" Prompt refresh complete.\n");
209
282
  }
210
283
  }
211
284
  // Step 3: Install native agent configs
212
- console.log('[3/8] Installing native agent configs...');
285
+ console.log("[3/8] Installing native agent configs...");
213
286
  {
214
- const agentConfigCount = await installNativeAgentConfigs(pkgRoot, {
215
- force,
287
+ summary.nativeAgents = await refreshNativeAgentConfigs(pkgRoot, scopeDirs.nativeAgentsDir, backupContext, {
216
288
  dryRun,
217
289
  verbose,
218
- agentsDir: scopeDirs.nativeAgentsDir,
219
290
  });
220
- console.log(` Installed ${agentConfigCount} native agent configs to ${scopeDirs.nativeAgentsDir}.\n`);
291
+ console.log(` Native agent refresh complete (${scopeDirs.nativeAgentsDir}).\n`);
221
292
  }
222
293
  // Step 4: Install skills
223
- console.log('[4/8] Installing skills...');
294
+ console.log("[4/8] Installing skills...");
224
295
  {
225
- const skillsSrc = join(pkgRoot, 'skills');
296
+ const skillsSrc = join(pkgRoot, "skills");
226
297
  const skillsDst = scopeDirs.skillsDir;
227
- const skillCount = await installSkills(skillsSrc, skillsDst, { force, dryRun, verbose });
298
+ summary.skills = await installSkills(skillsSrc, skillsDst, backupContext, {
299
+ force,
300
+ dryRun,
301
+ verbose,
302
+ });
228
303
  if (catalogCounts) {
229
- console.log(` Installed ${skillCount} skills (catalog baseline: ${catalogCounts.skills}).\n`);
304
+ console.log(` Skill refresh complete (catalog baseline: ${catalogCounts.skills}).\n`);
230
305
  }
231
306
  else {
232
- console.log(` Installed ${skillCount} skills.\n`);
307
+ console.log(" Skill refresh complete.\n");
233
308
  }
234
309
  }
235
310
  // Step 5: Update config.toml
236
- console.log('[5/8] Updating config.toml...');
237
- if (!dryRun) {
238
- await mergeConfig(scopeDirs.codexConfigFile, pkgRoot, {
239
- verbose,
240
- agentsConfigDir: scopeDirs.nativeAgentsDir,
241
- });
242
- }
243
- console.log(` Done (${scopeDirs.codexConfigFile}).\n`);
311
+ console.log("[5/8] Updating config.toml...");
312
+ await updateManagedConfig(scopeDirs.codexConfigFile, pkgRoot, scopeDirs.nativeAgentsDir, summary.config, backupContext, { dryRun, verbose, modelUpgradePrompt });
313
+ console.log(` Config refresh complete (${scopeDirs.codexConfigFile}).\n`);
244
314
  // Step 5.5: Verify team CLI interop surface is available.
245
- console.log('[5.5/8] Verifying Team CLI API interop...');
315
+ console.log("[5.5/8] Verifying Team CLI API interop...");
246
316
  const teamToolsCheck = await verifyTeamCliApiInterop(pkgRoot);
247
317
  if (teamToolsCheck.ok) {
248
- console.log(' omx team api command detected (CLI-first interop ready)');
318
+ console.log(" omx team api command detected (CLI-first interop ready)");
249
319
  }
250
320
  else {
251
321
  console.log(` WARNING: ${teamToolsCheck.message}`);
252
- console.log(' Run `npm run build` and then re-run `omx setup`.');
322
+ console.log(" Run `npm run build` and then re-run `omx setup`.");
253
323
  }
254
324
  console.log();
255
325
  // Step 6: Generate AGENTS.md
256
- console.log('[6/8] Generating AGENTS.md...');
257
- const agentsMdSrc = join(pkgRoot, 'templates', 'AGENTS.md');
258
- const agentsMdDst = join(projectRoot, 'AGENTS.md');
326
+ console.log("[6/8] Generating AGENTS.md...");
327
+ const agentsMdSrc = join(pkgRoot, "templates", "AGENTS.md");
328
+ const agentsMdDst = join(projectRoot, "AGENTS.md");
259
329
  const agentsMdExists = existsSync(agentsMdDst);
260
330
  // Guard: refuse to overwrite AGENTS.md during active session
261
331
  const activeSession = await readSessionState(projectRoot);
262
332
  const sessionIsActive = activeSession && !isSessionStale(activeSession);
263
333
  if (existsSync(agentsMdSrc)) {
264
- let shouldOverwriteAgentsMd = force;
265
- if (!force && agentsMdExists && process.stdin.isTTY && process.stdout.isTTY) {
266
- shouldOverwriteAgentsMd = agentsOverwritePrompt
267
- ? await agentsOverwritePrompt()
268
- : await promptForAgentsOverwrite();
269
- }
270
- if (sessionIsActive && shouldOverwriteAgentsMd) {
271
- console.log(' WARNING: Active omx session detected (pid ' + activeSession?.pid + ').');
272
- console.log(' Skipping AGENTS.md overwrite to avoid corrupting runtime overlay.');
273
- if (force) {
274
- console.log(' Stop the active session first, then re-run setup --force.');
275
- }
276
- else {
277
- console.log(' Stop the active session first, then re-run setup and approve overwrite (or use --force).');
278
- }
334
+ const content = await readFile(agentsMdSrc, "utf-8");
335
+ const rewritten = applyScopePathRewritesToAgentsTemplate(content, resolvedScope.scope);
336
+ let changed = true;
337
+ if (agentsMdExists) {
338
+ const existing = await readFile(agentsMdDst, "utf-8");
339
+ changed = existing !== rewritten;
279
340
  }
280
- else if (shouldOverwriteAgentsMd || !agentsMdExists) {
281
- if (!dryRun) {
282
- const content = await readFile(agentsMdSrc, 'utf-8');
283
- const rewritten = applyScopePathRewritesToAgentsTemplate(content, resolvedScope.scope);
284
- await writeFile(agentsMdDst, rewritten);
285
- }
286
- console.log(' Generated AGENTS.md in project root.');
341
+ if (sessionIsActive && agentsMdExists && changed) {
342
+ summary.agentsMd.skipped += 1;
343
+ console.log(" WARNING: Active omx session detected (pid " +
344
+ activeSession?.pid +
345
+ ").");
346
+ console.log(" Skipping AGENTS.md overwrite to avoid corrupting runtime overlay.");
347
+ console.log(" Stop the active session first, then re-run setup.");
287
348
  }
288
349
  else {
289
- console.log(' AGENTS.md already exists (use --force to overwrite).');
350
+ await syncManagedContent(rewritten, agentsMdDst, summary.agentsMd, backupContext, { dryRun, verbose }, "AGENTS.md");
351
+ if (summary.agentsMd.updated > 0) {
352
+ console.log(" Generated AGENTS.md in project root.");
353
+ }
354
+ else if (summary.agentsMd.unchanged > 0) {
355
+ console.log(" AGENTS.md already up to date.");
356
+ }
290
357
  }
291
358
  }
292
359
  else {
293
- console.log(' AGENTS.md template not found, skipping.');
360
+ summary.agentsMd.skipped += 1;
361
+ console.log(" AGENTS.md template not found, skipping.");
294
362
  }
295
363
  console.log();
296
364
  // Step 7: Set up notify hook
297
- console.log('[7/8] Configuring notification hook...');
365
+ console.log("[7/8] Configuring notification hook...");
298
366
  await setupNotifyHook(pkgRoot, { dryRun, verbose });
299
- console.log(' Done.\n');
367
+ console.log(" Done.\n");
300
368
  // Step 8: Configure HUD
301
- console.log('[8/8] Configuring HUD...');
302
- const hudConfigPath = join(projectRoot, '.omx', 'hud-config.json');
369
+ console.log("[8/8] Configuring HUD...");
370
+ const hudConfigPath = join(projectRoot, ".omx", "hud-config.json");
303
371
  if (force || !existsSync(hudConfigPath)) {
304
372
  if (!dryRun) {
305
- const defaultHudConfig = { preset: 'focused' };
373
+ const defaultHudConfig = { preset: "focused" };
306
374
  await writeFile(hudConfigPath, JSON.stringify(defaultHudConfig, null, 2));
307
375
  }
308
376
  if (verbose)
309
- console.log(' Wrote .omx/hud-config.json');
310
- console.log(' HUD config created (preset: focused).');
377
+ console.log(" Wrote .omx/hud-config.json");
378
+ console.log(" HUD config created (preset: focused).");
311
379
  }
312
380
  else {
313
- console.log(' HUD config already exists (use --force to overwrite).');
381
+ console.log(" HUD config already exists (use --force to overwrite).");
314
382
  }
315
- console.log(' StatusLine configured in config.toml via [tui] section.');
383
+ console.log(" StatusLine configured in config.toml via [tui] section.");
384
+ console.log();
385
+ console.log("Setup refresh summary:");
386
+ logCategorySummary("prompts", summary.prompts);
387
+ logCategorySummary("skills", summary.skills);
388
+ logCategorySummary("native_agents", summary.nativeAgents);
389
+ logCategorySummary("agents_md", summary.agentsMd);
390
+ logCategorySummary("config", summary.config);
316
391
  console.log();
392
+ if (force) {
393
+ console.log("Force mode: enabled additional destructive maintenance (for example stale deprecated skill cleanup).");
394
+ console.log();
395
+ }
317
396
  console.log('Setup complete! Run "omx doctor" to verify installation.');
318
- console.log('\nNext steps:');
319
- console.log(' 1. Start Codex CLI in your project directory');
320
- console.log(' 2. Use /prompts:architect, /prompts:executor, /prompts:planner as slash commands');
321
- console.log(' 3. Skills are available via /skills or implicit matching');
322
- console.log(' 4. The AGENTS.md orchestration brain is loaded automatically');
323
- console.log(' 5. Native agent roles registered in config.toml [agents.*]');
397
+ console.log("\nNext steps:");
398
+ console.log(" 1. Start Codex CLI in your project directory");
399
+ console.log(" 2. Use /prompts:architect, /prompts:executor, /prompts:planner as slash commands");
400
+ console.log(" 3. Skills are available via /skills or implicit matching");
401
+ console.log(" 4. The AGENTS.md orchestration brain is loaded automatically");
402
+ console.log(" 5. Native agent roles registered in config.toml [agents.*]");
324
403
  if (isGitHubCliConfigured()) {
325
- console.log('\nSupport the project: gh repo star Yeachan-Heo/oh-my-codex');
404
+ console.log("\nSupport the project: gh repo star Yeachan-Heo/oh-my-codex");
326
405
  }
327
406
  }
328
407
  function isLegacySkillPromptShim(content) {
@@ -332,19 +411,18 @@ function isLegacySkillPromptShim(content) {
332
411
  async function cleanupLegacySkillPromptShims(promptsSrcDir, promptsDstDir, options) {
333
412
  if (!existsSync(promptsSrcDir) || !existsSync(promptsDstDir))
334
413
  return 0;
335
- const sourceFiles = new Set((await readdir(promptsSrcDir))
336
- .filter(name => name.endsWith('.md')));
414
+ const sourceFiles = new Set((await readdir(promptsSrcDir)).filter((name) => name.endsWith(".md")));
337
415
  const installedFiles = await readdir(promptsDstDir);
338
416
  let removed = 0;
339
417
  for (const file of installedFiles) {
340
- if (!file.endsWith('.md'))
418
+ if (!file.endsWith(".md"))
341
419
  continue;
342
420
  if (sourceFiles.has(file))
343
421
  continue;
344
422
  const fullPath = join(promptsDstDir, file);
345
- let content = '';
423
+ let content = "";
346
424
  try {
347
- content = await readFile(fullPath, 'utf-8');
425
+ content = await readFile(fullPath, "utf-8");
348
426
  }
349
427
  catch {
350
428
  continue;
@@ -361,14 +439,56 @@ async function cleanupLegacySkillPromptShims(promptsSrcDir, promptsDstDir, optio
361
439
  return removed;
362
440
  }
363
441
  function isGitHubCliConfigured() {
364
- const result = spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' });
442
+ const result = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
365
443
  return result.status === 0;
366
444
  }
367
- async function installDirectory(srcDir, dstDir, ext, options) {
445
+ async function syncManagedFileFromDisk(srcPath, dstPath, summary, backupContext, options, verboseLabel) {
446
+ const destinationExists = existsSync(dstPath);
447
+ const changed = !destinationExists || (await filesDiffer(srcPath, dstPath));
448
+ if (!changed) {
449
+ summary.unchanged += 1;
450
+ return;
451
+ }
452
+ if (await ensureBackup(dstPath, destinationExists, backupContext, options)) {
453
+ summary.backedUp += 1;
454
+ }
455
+ if (!options.dryRun) {
456
+ await mkdir(dirname(dstPath), { recursive: true });
457
+ await copyFile(srcPath, dstPath);
458
+ }
459
+ summary.updated += 1;
460
+ if (options.verbose) {
461
+ console.log(` ${options.dryRun ? "would update" : "updated"} ${verboseLabel}`);
462
+ }
463
+ }
464
+ async function syncManagedContent(content, dstPath, summary, backupContext, options, verboseLabel) {
465
+ const destinationExists = existsSync(dstPath);
466
+ let changed = true;
467
+ if (destinationExists) {
468
+ const existing = await readFile(dstPath, "utf-8");
469
+ changed = existing !== content;
470
+ }
471
+ if (!changed) {
472
+ summary.unchanged += 1;
473
+ return;
474
+ }
475
+ if (await ensureBackup(dstPath, destinationExists, backupContext, options)) {
476
+ summary.backedUp += 1;
477
+ }
478
+ if (!options.dryRun) {
479
+ await mkdir(dirname(dstPath), { recursive: true });
480
+ await writeFile(dstPath, content);
481
+ }
482
+ summary.updated += 1;
483
+ if (options.verbose) {
484
+ console.log(` ${options.dryRun ? "would update" : "updated"} ${verboseLabel}`);
485
+ }
486
+ }
487
+ async function installDirectory(srcDir, dstDir, ext, backupContext, options, kindLabel) {
488
+ const summary = createEmptyCategorySummary();
368
489
  if (!existsSync(srcDir))
369
- return 0;
490
+ return summary;
370
491
  const files = await readdir(srcDir);
371
- let count = 0;
372
492
  for (const file of files) {
373
493
  if (!file.endsWith(ext))
374
494
  continue;
@@ -377,78 +497,67 @@ async function installDirectory(srcDir, dstDir, ext, options) {
377
497
  const srcStat = await stat(src);
378
498
  if (!srcStat.isFile())
379
499
  continue;
380
- if (options.force || !existsSync(dst)) {
381
- if (!options.dryRun) {
382
- await copyFile(src, dst);
383
- }
384
- if (options.verbose)
385
- console.log(` ${file}`);
386
- count++;
500
+ await syncManagedFileFromDisk(src, dst, summary, backupContext, options, `${kindLabel} ${file}`);
501
+ }
502
+ return summary;
503
+ }
504
+ async function refreshNativeAgentConfigs(pkgRoot, agentsDir, backupContext, options) {
505
+ const summary = createEmptyCategorySummary();
506
+ if (!options.dryRun) {
507
+ await mkdir(agentsDir, { recursive: true });
508
+ }
509
+ for (const [name, agent] of Object.entries(AGENT_DEFINITIONS)) {
510
+ const promptPath = join(pkgRoot, "prompts", `${name}.md`);
511
+ if (!existsSync(promptPath)) {
512
+ continue;
387
513
  }
514
+ const promptContent = await readFile(promptPath, "utf-8");
515
+ const toml = generateAgentToml(agent, promptContent);
516
+ const dst = join(agentsDir, `${name}.toml`);
517
+ await syncManagedContent(toml, dst, summary, backupContext, options, `native agent ${name}.toml`);
388
518
  }
389
- return count;
519
+ return summary;
390
520
  }
391
- async function installSkills(srcDir, dstDir, options) {
521
+ async function installSkills(srcDir, dstDir, backupContext, options) {
522
+ const summary = createEmptyCategorySummary();
392
523
  if (!existsSync(srcDir))
393
- return 0;
524
+ return summary;
394
525
  const manifest = tryReadCatalogManifest();
395
526
  const skillStatusByName = manifest
396
527
  ? new Map(manifest.skills.map((skill) => [skill.name, skill.status]))
397
528
  : null;
398
- const isInstallableStatus = (status) => status === 'active' || status === 'internal';
529
+ const isInstallableStatus = (status) => status === "active" || status === "internal";
399
530
  const entries = await readdir(srcDir, { withFileTypes: true });
400
531
  const staleCandidateSkillNames = new Set(manifest?.skills.map((skill) => skill.name) ?? []);
401
- let count = 0;
402
532
  for (const entry of entries) {
403
533
  if (!entry.isDirectory())
404
534
  continue;
405
535
  staleCandidateSkillNames.add(entry.name);
406
536
  const status = skillStatusByName?.get(entry.name);
407
537
  if (skillStatusByName && !isInstallableStatus(status)) {
538
+ summary.skipped += 1;
408
539
  if (options.verbose) {
409
- const label = status ?? 'unlisted';
540
+ const label = status ?? "unlisted";
410
541
  console.log(` skipped ${entry.name}/ (status: ${label})`);
411
542
  }
412
543
  continue;
413
544
  }
414
545
  const skillSrc = join(srcDir, entry.name);
415
546
  const skillDst = join(dstDir, entry.name);
416
- const skillMd = join(skillSrc, 'SKILL.md');
547
+ const skillMd = join(skillSrc, "SKILL.md");
417
548
  if (!existsSync(skillMd))
418
549
  continue;
419
- let copied = 0;
420
- let overwritten = 0;
421
- let skipped = 0;
422
- const skillFiles = await readdir(skillSrc);
423
550
  if (!options.dryRun) {
424
551
  await mkdir(skillDst, { recursive: true });
425
552
  }
553
+ const skillFiles = await readdir(skillSrc);
426
554
  for (const sf of skillFiles) {
427
555
  const sfPath = join(skillSrc, sf);
428
556
  const sfStat = await stat(sfPath);
429
557
  if (!sfStat.isFile())
430
558
  continue;
431
559
  const dstPath = join(skillDst, sf);
432
- const dstExists = existsSync(dstPath);
433
- if (dstExists && !options.force) {
434
- skipped++;
435
- continue;
436
- }
437
- if (!options.dryRun) {
438
- await copyFile(sfPath, dstPath);
439
- }
440
- if (dstExists) {
441
- overwritten++;
442
- }
443
- else {
444
- copied++;
445
- }
446
- }
447
- if (copied + overwritten > 0) {
448
- count++;
449
- }
450
- if (options.verbose) {
451
- console.log(` ${entry.name}/ (copied: ${copied}, overwritten: ${overwritten}, skipped: ${skipped})`);
560
+ await syncManagedFileFromDisk(sfPath, dstPath, summary, backupContext, options, `skill ${entry.name}/${sf}`);
452
561
  }
453
562
  }
454
563
  if (options.force && manifest && existsSync(dstDir)) {
@@ -462,20 +571,68 @@ async function installSkills(srcDir, dstDir, options) {
462
571
  if (!options.dryRun) {
463
572
  await rm(staleSkillDir, { recursive: true, force: true });
464
573
  }
574
+ summary.removed += 1;
465
575
  if (options.verbose) {
466
- const prefix = options.dryRun ? 'would remove stale skill' : 'removed stale skill';
467
- const label = status ?? 'unlisted';
576
+ const prefix = options.dryRun
577
+ ? "would remove stale skill"
578
+ : "removed stale skill";
579
+ const label = status ?? "unlisted";
468
580
  console.log(` ${prefix} ${staleSkill}/ (status: ${label})`);
469
581
  }
470
582
  }
471
583
  }
472
- return count;
584
+ return summary;
585
+ }
586
+ async function updateManagedConfig(configPath, pkgRoot, agentsConfigDir, summary, backupContext, options) {
587
+ const existing = existsSync(configPath)
588
+ ? await readFile(configPath, "utf-8")
589
+ : "";
590
+ const currentModel = getRootModelName(existing);
591
+ let modelOverride;
592
+ if (currentModel === LEGACY_SETUP_MODEL) {
593
+ const shouldPrompt = typeof options.modelUpgradePrompt === "function" ||
594
+ (process.stdin.isTTY && process.stdout.isTTY);
595
+ if (shouldPrompt) {
596
+ const shouldUpgrade = options.modelUpgradePrompt
597
+ ? await options.modelUpgradePrompt(currentModel, DEFAULT_SETUP_MODEL)
598
+ : await promptForModelUpgrade(currentModel, DEFAULT_SETUP_MODEL);
599
+ if (shouldUpgrade) {
600
+ modelOverride = DEFAULT_SETUP_MODEL;
601
+ }
602
+ }
603
+ }
604
+ const finalConfig = buildMergedConfig(existing, pkgRoot, {
605
+ agentsConfigDir,
606
+ modelOverride,
607
+ verbose: options.verbose,
608
+ });
609
+ const changed = existing !== finalConfig;
610
+ if (!changed) {
611
+ summary.unchanged += 1;
612
+ return;
613
+ }
614
+ if (await ensureBackup(configPath, existsSync(configPath), backupContext, options)) {
615
+ summary.backedUp += 1;
616
+ }
617
+ if (!options.dryRun) {
618
+ await writeFile(configPath, finalConfig);
619
+ }
620
+ if (options.verbose &&
621
+ modelOverride &&
622
+ currentModel &&
623
+ currentModel !== modelOverride) {
624
+ console.log(` ${options.dryRun ? "would update" : "updated"} root model from ${currentModel} to ${modelOverride}`);
625
+ }
626
+ summary.updated += 1;
627
+ if (options.verbose) {
628
+ console.log(` ${options.dryRun ? "would update" : "updated"} config ${configPath}`);
629
+ }
473
630
  }
474
631
  async function setupNotifyHook(pkgRoot, options) {
475
- const hookScript = join(pkgRoot, 'scripts', 'notify-hook.js');
632
+ const hookScript = join(pkgRoot, "scripts", "notify-hook.js");
476
633
  if (!existsSync(hookScript)) {
477
634
  if (options.verbose)
478
- console.log(' Notify hook script not found, skipping.');
635
+ console.log(" Notify hook script not found, skipping.");
479
636
  return;
480
637
  }
481
638
  // The notify hook is configured in config.toml via mergeConfig
@@ -483,15 +640,18 @@ async function setupNotifyHook(pkgRoot, options) {
483
640
  console.log(` Notify hook: ${hookScript}`);
484
641
  }
485
642
  async function verifyTeamCliApiInterop(pkgRoot) {
486
- const teamCliPath = join(pkgRoot, 'dist', 'cli', 'team.js');
643
+ const teamCliPath = join(pkgRoot, "dist", "cli", "team.js");
487
644
  if (!existsSync(teamCliPath)) {
488
645
  return { ok: false, message: `missing ${teamCliPath}` };
489
646
  }
490
647
  try {
491
- const content = await readFile(teamCliPath, 'utf-8');
648
+ const content = await readFile(teamCliPath, "utf-8");
492
649
  const missing = REQUIRED_TEAM_CLI_API_MARKERS.filter((marker) => !content.includes(marker));
493
650
  if (missing.length > 0) {
494
- return { ok: false, message: `team CLI interop markers missing: ${missing.join(', ')}` };
651
+ return {
652
+ ok: false,
653
+ message: `team CLI interop markers missing: ${missing.join(", ")}`,
654
+ };
495
655
  }
496
656
  return { ok: true };
497
657
  }