codex-1up 0.1.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kevin Kern
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to do so, subject to the
10
+ following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Codex CLI 1UP
2
+
3
+ ![codex-1up banner] (https://raw.githubusercontent.com/regenrek/codex-1up/main/public/banner.png)
4
+
5
+
6
+ **Codex 1UP** equips your Codex CLI coding agent with powerful tools.
7
+
8
+ - ✅ Installs/updates **Codex CLI** (`@openai/codex`)
9
+ - ✅ Adds fast shell power tools: `ast-grep`, `fd`, `ripgrep`, `rg`, `fzf`, `jq`, `yq`
10
+ - ✅ **AGENTS.md** template with tool selection guide
11
+ - ✅ Unified **Codex config** with multiple profiles: `balanced` / `safe` / `minimal` / `yolo`
12
+ - ✅ 🔊 **Notification sounds** with customizable audio alerts for Codex events
13
+
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # Install globally (recommended)
19
+ npm install -g codex-1up
20
+ codex-1up install
21
+ ```
22
+
23
+ ### After installing
24
+
25
+ - Open a new terminal session (or source your shell rc)
26
+ - Run `codex` to sign in and start using the agent! 🎉
27
+
28
+ ### What gets installed
29
+
30
+ | Component | Why it matters |
31
+ | ------------------------- | --------------------------------------------------------------------------------------- |
32
+ | **@openai/codex** | The coding agent that can read, edit, and run your project locally. |
33
+ | **ast-grep** | Syntax‑aware search/replace for safe, large‑scale refactors in TS/TSX. |
34
+ | **fd** | Fast file finder (gitignore‑aware). |
35
+ | **ripgrep (rg)** | Fast text search across code. |
36
+ | **fzf** | Fuzzy‑finder to select among many matches. |
37
+ | **jq** / **yq** | Reliable JSON/YAML processing on the command line. |
38
+ | **\~/.codex/config.toml** | Single template with multiple profiles. Active profile is chosen during install (default: `balanced`). See [Codex config reference](https://github.com/openai/codex/blob/main/docs/config.md). |
39
+ | **AGENTS.md** | Minimal per‑repo rubric; installer can also create global `~/.codex/AGENTS.md`. |
40
+ | **\~/.codex/notify.sh** | Notification hook script with customizable sounds for Codex events (default: `noti_1.wav`). |
41
+
42
+
43
+ ### Profiles
44
+
45
+ | Profile | Description |
46
+ | --- | --- |
47
+ | balanced (default) | Approvals on-request; workspace-write sandbox with network access inside workspace. |
48
+ | safe | Approvals on-failure; workspace-write sandbox; conservative. |
49
+ | minimal | Minimal reasoning effort; concise summaries; web search off. |
50
+ | yolo | Never ask for approvals; danger-full-access (only trusted environments). |
51
+
52
+ Switch profiles anytime: `codex --profile <name>` for a session, or `codex-1up config set-profile <name>` to persist.
53
+
54
+ ## Global guidance with AGENTS.md (optional)
55
+
56
+ You can keep a global guidance file at `~/.codex/AGENTS.md` that Codex will use across projects. During install, you’ll be prompted to create this; if you skip, you can create it later:
57
+
58
+ ```bash
59
+ # Create the directory if needed and write the template there
60
+ mkdir -p ~/.codex
61
+ ./bin/codex-1up agents --path ~/.codex
62
+ # This writes ~/.codex/AGENTS.md
63
+ ```
64
+
65
+ See memory behavior with AGENTS.md in the official docs: [Memory with AGENTS.md](https://github.com/openai/codex/blob/main/docs/getting-started.md#memory-with-agentsmd).
66
+
67
+ ### Notes
68
+ - Global npm packages (`@openai/codex`, `@ast-grep/cli`) are checked and only missing/outdated versions are installed.
69
+
70
+ ## Doctor & Uninstall
71
+
72
+ ```bash
73
+ ./bin/codex-1up doctor
74
+ ./bin/codex-1up uninstall
75
+ ```
76
+
77
+ > **Note:** This project is **idempotent**—running it again will skip what’s already installed. It won’t remove packages on uninstall; it cleans up files under ~/.codex (backups are retained).
78
+
79
+ ## Supported platforms
80
+
81
+ - macOS (Intel/Apple Silicon) via **Homebrew**
82
+ - Linux via **apt**, **dnf**, **pacman**, or **zypper**
83
+ - Windows users: use **WSL** (Ubuntu) and run the Linux path
84
+
85
+ ### Common flags
86
+
87
+ - `--shell auto|zsh|bash|fish`
88
+ - `--vscode EXT_ID` : install a VS Code extension (e.g. `openai.codex`)
89
+ - `--agents-md [PATH]` : write a starter `AGENTS.md` to PATH (default: `$PWD/AGENTS.md`)
90
+ - `--no-vscode` : skip VS Code extension checks
91
+ - `--install-node nvm|brew|skip` : how to install Node.js if missing (default: `nvm`)
92
+
93
+ ### Advanced / CI flags
94
+
95
+ - `--dry-run` : print what would happen, change nothing
96
+ - `--skip-confirmation` : suppress interactive prompts
97
+ - `--yes` : non-interactive, accept safe defaults (CI). Most users don’t need this.
98
+
99
+ ## Develop locally (from source)
100
+
101
+ For contributors and advanced users:
102
+
103
+ ```bash
104
+ git clone https://github.com/regenrek/codex-1up
105
+ cd codex-1up
106
+
107
+ # Use the wrapper to run the same flow as the global CLI
108
+ ./bin/codex-1up install
109
+
110
+ # Or run the CLI package directly in dev
111
+ cd cli && corepack enable && pnpm i && pnpm build
112
+ node ./bin/codex-1up.mjs install
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT — see [LICENSE](LICENSE).
118
+
119
+ ## Links
120
+
121
+ - X/Twitter: [@kregenrek](https://x.com/kregenrek)
122
+ - Bluesky: [@kevinkern.dev](https://bsky.app/profile/kevinkern.dev)
123
+
124
+ ## Courses
125
+ - Learn Cursor AI: [Ultimate Cursor Course](https://www.instructa.ai/en/cursor-ai)
126
+ - Learn to build software with AI: [AI Builder Hub](https://www.instructa.ai)
127
+
128
+ ## See my other projects:
129
+
130
+ * [codefetch](https://github.com/regenrek/codefetch) - Turn code into Markdown for LLMs with one simple terminal command
131
+ * [instructa](https://github.com/orgs/instructa/repositories) - Instructa Projects
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Loader: prefer built build; fallback to tsx in dev
3
+ import { createRequire } from 'module'
4
+ import { fileURLToPath } from 'url'
5
+ import { dirname, resolve } from 'path'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+ const dist = resolve(__dirname, '../dist/main.js')
9
+ const src = resolve(__dirname, '../src/main.ts')
10
+ const require = createRequire(import.meta.url)
11
+
12
+ const { existsSync } = require('fs')
13
+
14
+ if (existsSync(dist)) {
15
+ await import(dist)
16
+ } else {
17
+ // Dev fallback
18
+ await import('tsx/esm')
19
+ await import(src)
20
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/main.js ADDED
@@ -0,0 +1,31 @@
1
+ import{runMain as Ft}from"citty";import{defineCommand as Ot}from"citty";import{defineCommand as ft}from"citty";import{fileURLToPath as ct}from"url";import{dirname as dt,resolve as N}from"path";import{promises as U}from"fs";import*as L from"os";import{accessSync as Fe}from"fs";import*as Le from"toml";import*as p from"@clack/prompts";import{which as G,$ as x}from"zx";import rt from"fs-extra";import*as re from"path";import*as De from"os";import{createWriteStream as Ke}from"fs";function de(e){let t=null;try{t=Ke(e,{flags:"a"})}catch{}let o=(i,n)=>{let r=i?`${i} ${n}
2
+ `:`${n}
3
+ `;process.stdout.write(r),t&&t.write(r)};return{log:i=>o("",i),info:i=>o("",i),ok:i=>o("\u2714",i),warn:i=>o("\u26A0",i),err:i=>o("\u2716",i)}}import{$ as F}from"zx";import{which as Ye}from"zx";import{spawn as Qe}from"child_process";async function d(e){try{return await Ye(e),!0}catch{return!1}}async function ue(){return await d("brew")?"brew":await d("apt-get")?"apt":await d("dnf")?"dnf":await d("pacman")?"pacman":await d("zypper")?"zypper":"none"}async function m(e,t,o={dryRun:!1}){if(o.dryRun){let n=[e,...t].map(r=>r.includes(" ")?`"${r}"`:r).join(" ");o.logger?.log(`[dry-run] ${n}`);return}let i=Qe(e,t,{stdio:"inherit",cwd:o.cwd||process.cwd(),shell:!1});await new Promise((n,r)=>{i.on("error",r),i.on("exit",s=>{if(s===0)return n();r(new Error(`Command failed (${s}): ${e} ${t.join(" ")}`))})})}function D(e){let t=new Date().toISOString().replace(/[:.]/g,"-").slice(0,-5);return`${e}.backup.${t}`}import ge from"fs-extra";import*as q from"path";import*as W from"os";async function me(e){let t=await d("node"),o=await d("npm");if(t&&o){let n=(await F`node -v`).stdout.trim();e.logger.ok(`Node.js present (${n})`);return}switch(e.options.installNode){case"nvm":await Ze(e);break;case"brew":await et(e);break;case"skip":e.logger.warn("Skipping Node installation; please install Node 18+ manually");return}if(await d("node")){let n=(await F`node -v`).stdout.trim();e.logger.ok(`Node.js installed (${n})`)}else throw e.logger.err("Node installation failed"),new Error("Node.js installation failed")}async function Ze(e){if(e.logger.info("Installing Node.js via nvm"),e.options.dryRun){e.logger.log("[dry-run] install nvm + Node LTS");return}let t=q.join(e.homeDir,".nvm"),o=q.join(t,"nvm.sh");await ge.pathExists(t)||(e.logger.info("Installing nvm..."),await F`bash -c "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"`);let i=`export NVM_DIR="${t}" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install --lts`;await F`bash -c ${i}`}async function et(e){if(e.logger.info("Installing Node.js via Homebrew"),!await d("brew"))if(W.platform()==="darwin"){if(e.logger.info("Homebrew not found; installing Homebrew"),e.options.dryRun){e.logger.log("[dry-run] install Homebrew");return}await F`/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`;let t=W.platform()==="darwin"&&W.arch()==="arm64"?"/opt/homebrew/bin/brew":"/usr/local/bin/brew";if(await ge.pathExists(t)){let o=q.dirname(t);process.env.PATH=`${o}:${process.env.PATH||""}`;try{let n=(await F`${t} shellenv`).stdout.trim().match(/export PATH="([^"]+)"/);n&&(process.env.PATH=`${n[1]}:${process.env.PATH||""}`)}catch{}}}else throw new Error("Homebrew is only available on macOS");await m("brew",["install","node"],{dryRun:e.options.dryRun,logger:e.logger})}import{$ as oe}from"zx";var tt=["@openai/codex","@ast-grep/cli"];async function we(e){e.logger.info("Checking global npm packages (@openai/codex, @ast-grep/cli)");let t=[];for(let o of tt)try{let n=(await oe`npm view ${o} version`.quiet()).stdout.trim();if(!n){e.logger.warn(`Could not fetch latest version for ${o}; skipping upgrade check`);continue}let r=await oe`npm ls -g ${o} --depth=0 --json`.quiet().nothrow(),s="";try{s=JSON.parse(r.stdout||"{}").dependencies?.[o]?.version||""}catch{s=""}s?s!==n?(e.logger.info(`${o} ${s} -> ${n}`),t.push(`${o}@${n}`)):e.logger.ok(`${o} up-to-date (${s})`):(e.logger.info(`${o} not installed; will install @${n}`),t.push(`${o}@${n}`))}catch(i){e.logger.warn(`Error checking ${o}: ${i}`);let n=await oe`npm ls -g ${o} --depth=0 --json`.quiet().nothrow(),r="";try{r=JSON.parse(n.stdout||"{}").dependencies?.[o]?.version||""}catch{r=""}r||t.push(o)}t.length>0?(e.logger.info("Installing/updating global npm packages"),await m("npm",["install","-g",...t],{dryRun:e.options.dryRun,logger:e.logger})):e.logger.ok("Global npm packages are up-to-date"),await d("codex")?e.logger.ok("Codex CLI installed"):e.logger.err("Codex CLI not found after install"),await d("ast-grep")?e.logger.ok("ast-grep installed"):e.logger.warn("ast-grep not found; check npm global path")}import{$ as ot}from"zx";import*as ie from"path";import ne from"fs-extra";var nt={brew:["fd","ripgrep","fzf","jq","yq","difftastic"],apt:["ripgrep","fzf","jq","yq","git-delta"],dnf:["ripgrep","fd-find","fzf","jq","yq","git-delta"],pacman:["ripgrep","fd","fzf","jq","yq","git-delta"],zypper:["ripgrep","fd","fzf","jq","yq","git-delta"],none:[]};async function he(e){let t=await ue();if(e.logger.info(`Detected package manager: ${t}`),t==="none"){e.logger.warn("Could not detect a supported package manager; please install tools manually");return}let o=nt[t]||[];if(o.length>0)switch(t){case"brew":await m("brew",["update"],{dryRun:e.options.dryRun,logger:e.logger}),await m("brew",["install",...o],{dryRun:e.options.dryRun,logger:e.logger});break;case"apt":await m("sudo",["apt-get","update","-y"],{dryRun:e.options.dryRun,logger:e.logger}),await m("sudo",["apt-get","install","-y",...o],{dryRun:e.options.dryRun,logger:e.logger}).catch(()=>{}),await d("fd")||await m("sudo",["apt-get","install","-y","fd-find"],{dryRun:e.options.dryRun,logger:e.logger}).catch(()=>{});break;case"dnf":await m("sudo",["dnf","install","-y",...o],{dryRun:e.options.dryRun,logger:e.logger}).catch(()=>{});break;case"pacman":await m("sudo",["pacman","-Sy","--noconfirm",...o],{dryRun:e.options.dryRun,logger:e.logger}).catch(()=>{});break;case"zypper":await m("sudo",["zypper","refresh"],{dryRun:e.options.dryRun,logger:e.logger}),await m("sudo",["zypper","install","-y",...o],{dryRun:e.options.dryRun,logger:e.logger}).catch(()=>{});break}if(!await d("difft")&&!await d("difftastic")&&(await d("cargo")?(e.logger.info("Installing difftastic via cargo"),await m("cargo",["install","difftastic"],{dryRun:e.options.dryRun,logger:e.logger})):e.logger.warn("difftastic not found and Rust/cargo missing; falling back to git-delta")),await d("fdfind")&&!await d("fd")){let n=ie.join(e.homeDir,".local","bin");await ne.ensureDir(n);let r=(await ot`command -v fdfind`).stdout.trim(),s=ie.join(n,"fd");await ne.pathExists(s)||(e.options.dryRun?e.logger.log(`[dry-run] ln -s ${r} ${s}`):await ne.symlink(r,s),e.logger.ok("fd alias created at ~/.local/bin/fd"))}let i=["fd","fdfind","rg","fzf","jq","yq","difft","difftastic","delta","ast-grep"];for(let n of i)await d(n)&&e.logger.ok(`${n} \u2713`)}import I from"fs-extra";import*as B from"path";async function be(e){let t=B.join(e.homeDir,".codex","config.toml"),o=B.join(e.rootDir,"templates","codex-config.toml");if(await I.ensureDir(B.dirname(t)),!await I.pathExists(o))throw e.logger.err(`Unified config template missing at ${o}`),new Error(`Template not found: ${o}`);if(!await I.pathExists(t)){e.logger.info(`Creating unified Codex config with multiple profiles at ${t}`),e.options.dryRun?e.logger.log(`[dry-run] cp ${o} ${t}`):await I.copy(o,t),e.logger.ok("Created ~/.codex/config.toml"),await ye(t,e.options.profile,e),e.logger.info("Tip: use 'codex --profile <name>' to switch at runtime or 'codex-1up config set-profile <name>' to persist.");return}e.logger.warn("~/.codex/config.toml already exists");let i=e.options.overwriteConfig;if(i==="no"){e.logger.info("Keeping existing config unchanged");return}let n=!1;if(i==="yes")n=!0;else if(!e.options.assumeYes&&!e.options.skipConfirmation){n=!1,e.logger.info("Keeping existing config; you can manage profiles via the new CLI later.");return}if(n){let r=D(t);e.options.dryRun?e.logger.log(`[dry-run] cp ${t} ${r}`):await I.copy(t,r),e.logger.info(`Backed up to ${r}`),e.options.dryRun?e.logger.log(`[dry-run] cp ${o} ${t}`):await I.copy(o,t),e.logger.ok("Overwrote ~/.codex/config.toml with unified template"),await ye(t,e.options.profile,e)}}async function ye(e,t,o){if(o.logger.info(`Setting active profile to: ${t}`),o.options.dryRun){o.logger.log(`[dry-run] set profile = "${t}" in ${e}`);return}let n=(await I.readFile(e,"utf8")).split(`
4
+ `),r=!1,s=n.map(a=>/^\s*profile\s*=/.test(a)?(r=!0,`profile = "${t}"`):a);r||s.unshift(`profile = "${t}"`),await I.writeFile(e,s.join(`
5
+ `),"utf8")}import E from"fs-extra";import*as _ from"path";async function $e(e){let t=_.join(e.homeDir,".codex","notify.sh"),o=_.join(e.rootDir,"templates","notification.sh");await E.ensureDir(_.dirname(t));let i=e.options.notify;if(i==="no"){e.logger.info("Skipping notify hook installation");return}if(!await E.pathExists(o)){e.logger.warn(`Notification template missing at ${o}; skipping notify hook install`);return}if(await E.pathExists(t))if(i==="yes"){let r=D(t);e.options.dryRun?e.logger.log(`[dry-run] cp ${t} ${r}`):await E.copy(t,r),e.options.dryRun?e.logger.log(`[dry-run] cp ${o} ${t}`):(await E.copy(o,t),await E.chmod(t,493)),e.logger.ok("Updated notify hook (backup created)")}else!e.options.assumeYes&&!e.options.skipConfirmation&&e.logger.info("Keeping existing notify hook");else e.options.dryRun?e.logger.log(`[dry-run] cp ${o} ${t}`):(await E.copy(o,t),await E.chmod(t,493)),e.logger.ok("Installed notify hook to ~/.codex/notify.sh");let n=_.join(e.homeDir,".codex","config.toml");await E.pathExists(n)?await it(n,t,e):e.logger.warn(`Config not found at ${n}; run again after config is created`)}async function it(e,t,o){if(o.options.dryRun){o.logger.log("[dry-run] update config notify and tui.notifications");return}let n=(await E.readFile(e,"utf8")).split(/\r?\n/),r="",s=null,a=null,c=!1,R=!1;for(let l=0;l<n.length;l++){let f=n[l],k=f.match(/^\s*\[([^\]]+)\]\s*$/);if(k){r=k[1],r==="tui"&&(a=l);continue}let g=/^\s*notify\s*=\s*\[/.test(f),A=/^\s*tui\.notifications\s*=/.test(f),j=/^\s*notifications\s*=/.test(f);g&&(r===""?s===null&&(s=l):/^profiles\.[^.]+\.features$/.test(r)&&(n.splice(l,1),l--,c=!0)),A&&/^profiles\.[^.]+\.features$/.test(r)&&(n.splice(l,1),l--,R=!0),j&&r===""&&(n.splice(l,1),l--,R=!0)}function $(l){let f=0;for(;f<n.length&&/^\s*(#.*)?$/.test(n[f]);)f++;n.splice(f,0,l)}if(s!==null){let l=n[s].match(/^(\s*notify\s*=\s*\[)([^\]]*)\]/);if(l){let f=l[2].trim();if(!f.includes(JSON.stringify(t))){let g=f&&!f.endsWith(",")?", ":"";n[s]=`${l[1]}${f}${g}${JSON.stringify(t)}]`,o.logger.ok("Added notify hook to config")}}}else $(`notify = [${JSON.stringify(t)}]`),o.logger.ok("Enabled notify hook in config");let w=!1;if(a!==null){let l=a+1,f=!1;for(;l<n.length;l++){let k=n[l];if(/^\s*\[/.test(k))break;if(/^\s*notifications\s*=/.test(k)){n[l]="notifications = true",f=!0,w=!0;break}}f||(n.splice(a+1,0,"notifications = true"),w=!0)}w||$(`[tui]
6
+ notifications = true`);let S=[];r="";for(let l=0;l<n.length;l++){let f=n[l],k=f.match(/^\s*\[([^\]]+)\]\s*$/);if(k){r=k[1],S.push(f);continue}/^\s*tui\.notifications\s*=/.test(f)||/^\s*notifications\s*=/.test(f)&&r!=="tui"||S.push(f)}await E.writeFile(e,S.join(`
7
+ `),"utf8")}import v from"fs-extra";import*as u from"path";async function Ce(e){let t=u.join(e.rootDir,"sounds"),o=u.join(e.homeDir,".codex","sounds");await v.ensureDir(o);let i=e.options.notificationSound;if(i==="none"){let w=await ke(e);e.options.dryRun?e.logger.log(`[dry-run] update ${w}`):await ve(w,`# Notification sound (disabled)
8
+ export CODEX_DISABLE_SOUND=1
9
+ export CODEX_CUSTOM_SOUND=""
10
+ `,e);let l=u.join(e.homeDir,".codex","notify.sh");if(await v.pathExists(l)){let f=await v.readFile(l,"utf8"),k=f.replace(/^DEFAULT_CODEX_SOUND=.*$/m,'DEFAULT_CODEX_SOUND=""');e.options.dryRun?e.logger.log(`[dry-run] patch ${l} DEFAULT_CODEX_SOUND -> empty`):k!==f&&await v.writeFile(l,k,"utf8")}e.logger.ok("Notification sound disabled");return}let n;if(i&&!u.isAbsolute(i)?n=u.join(t,i):i?n=i:e.options.mode==="recommended"&&(n=u.join(t,"noti_1.wav")),!n||!await v.pathExists(n)){e.logger.warn("No notification sound selected or file missing; skipping sound setup");return}let r=u.isAbsolute(n),s=n.startsWith(u.join(e.rootDir,"sounds")),a=!r||s?u.join(o,u.basename(n)):n;(!r||s)&&(e.options.dryRun?e.logger.log(`[dry-run] cp ${n} ${a}`):await v.copy(n,a));let c=await ke(e),R=`# Notification sound
11
+ export CODEX_DISABLE_SOUND=0
12
+ export CODEX_CUSTOM_SOUND="${a}"
13
+ `;e.options.dryRun?e.logger.log(`[dry-run] update ${c}`):await ve(c,R,e),e.logger.ok("Notification sound configured. Open a new shell or source your rc to apply.");let $=u.join(e.homeDir,".codex","notify.sh");if(await v.pathExists($)){let w=await v.readFile($,"utf8"),S=`DEFAULT_CODEX_SOUND="${a}"`,l=w.replace(/^DEFAULT_CODEX_SOUND=.*$/m,S);l!==w&&(e.options.dryRun?e.logger.log(`[dry-run] patch ${$} DEFAULT_CODEX_SOUND -> ${a}`):await v.writeFile($,l,"utf8"))}}async function ke(e){let t=e.options.shell||process.env.SHELL||"",o="auto";switch(o==="auto"&&(t.includes("zsh")?o="zsh":t.includes("fish")?o="fish":o="bash"),o){case"zsh":return u.join(e.homeDir,".zshrc");case"fish":return u.join(e.homeDir,".config","fish","config.fish");default:return u.join(e.homeDir,".bashrc")}}async function ve(e,t,o){let i="codex-1up";await v.ensureDir(u.dirname(e));let n="";if(await v.pathExists(e)){n=await v.readFile(e,"utf8");let s=`>>> ${i} >>>`,a=`<<< ${i} <<<`,c=new RegExp(`${s}[\\s\\S]*?${a}\\n?`,"g");n=n.replace(c,"")}let r=`>>> ${i} >>>
14
+ ${t}<<< ${i} <<<
15
+ `;await v.writeFile(e,n+r,"utf8")}import C from"fs-extra";import*as P from"path";async function Se(e){let t=P.join(e.homeDir,".codex","AGENTS.md"),o=e.options.globalAgents;if(o==="skip"){e.logger.info("Skipping global AGENTS.md creation");return}let i=P.join(e.rootDir,"templates","agent-templates","AGENTS-default.md");if(!await C.pathExists(i)){e.logger.warn(`Template not found at ${i}`);return}switch(o){case"create-default":if(await C.pathExists(t)){e.logger.info("Global AGENTS.md already exists; leaving unchanged");return}await C.ensureDir(P.dirname(t)),e.options.dryRun?e.logger.log(`[dry-run] cp ${i} ${t}`):await C.copy(i,t),e.logger.ok(`Wrote ${t}`);break;case"overwrite-default":if(await C.ensureDir(P.dirname(t)),await C.pathExists(t)){let r=D(t);e.options.dryRun?e.logger.log(`[dry-run] cp ${t} ${r}`):await C.copy(t,r),e.logger.info(`Backed up existing AGENTS.md to: ${r}`)}e.options.dryRun?e.logger.log(`[dry-run] cp ${i} ${t}`):await C.copy(i,t),e.logger.ok(`Wrote ${t}`);break;case"append-default":if(await C.ensureDir(P.dirname(t)),await C.pathExists(t)){let r=D(t);e.options.dryRun?e.logger.log(`[dry-run] cp ${t} ${r}`):await C.copy(t,r),e.logger.info(`Backed up existing AGENTS.md to: ${r}`)}let n=await C.readFile(i,"utf8");e.options.dryRun?e.logger.log(`[dry-run] append template to ${t}`):await C.appendFile(t,`
16
+ ---
17
+
18
+ ${n}`,"utf8"),e.logger.ok(`Appended template to ${t}`);break;default:e.options.skipConfirmation&&e.logger.info("Skipping global AGENTS.md creation (non-interactive mode)");break}}async function Re(e){if(e.options.noVscode)return;let t=e.options.vscodeId;if(!t){e.logger.info("VS Code extension id not provided. Use: --vscode <publisher.extension>");return}if(!await d("code")){e.logger.warn("'code' (VS Code) not in PATH; skipping extension install");return}if(e.options.dryRun){e.logger.log(`[dry-run] code --install-extension ${t}`);return}e.logger.info(`Installing VS Code extension: ${t}`),await m("code",["--install-extension",t,"--force"],{dryRun:!1,logger:e.logger}),e.logger.ok(`VS Code extension '${t}' installed (or already present)`)}import V from"fs-extra";import*as z from"path";async function Ee(e){let t=e.options.agentsMd;if(!t)return;let o=t;(await V.stat(o).catch(()=>null))?.isDirectory()&&(o=z.join(o,"AGENTS.md"));let i=z.join(e.rootDir,"templates","agent-templates","AGENTS-default.md");if(await V.pathExists(o)){e.logger.warn(`${o} already exists`);let n=D(o);e.options.dryRun?e.logger.log(`[dry-run] cp ${o} ${n}`):await V.copy(o,n),e.logger.info(`Backed up existing AGENTS.md to: ${n}`)}e.logger.info(`Writing starter AGENTS.md to: ${o}`),e.options.dryRun?e.logger.log(`[dry-run] write AGENTS.md to ${o}`):(await V.ensureDir(z.dirname(o)),await V.copy(i,o),e.logger.ok("Wrote AGENTS.md"))}var Ne="codex-1up";async function ae(e,t){let o=De.homedir(),i=re.join(o,`.${Ne}`);await rt.ensureDir(i);let n=new Date().toISOString().replace(/[:.]/g,"-").slice(0,-5),r=re.join(i,`install-${n}.log`),s=de(r);s.info(`==> ${Ne} installer`),s.info(`Log: ${r}`);let a={cwd:process.cwd(),homeDir:o,rootDir:t,logDir:i,logFile:r,options:e,logger:s};try{await me(a),await we(a),await he(a),await be(a),await $e(a),await Ce(a),await Se(a),await Re(a),await Ee(a),s.ok("All done. Open a new shell or 'source' your rc file to load aliases."),s.info("Next steps:"),s.info(" 1) codex # sign in; then ask it to plan a refactor"),s.info(" 2) ./bin/codex-1up agents --path $PWD # write a starter AGENTS.md to your repo"),s.info(" 3) Review ~/.codex/config.toml (see: https://github.com/openai/codex/blob/main/docs/config.md)")}catch(c){throw s.err(`Installation failed: ${c}`),c}}import{defineCommand as Q}from"citty";import{promises as H}from"fs";import{resolve as T,dirname as je}from"path";import{fileURLToPath as at}from"url";import{accessSync as Ie}from"fs";import st from"os";var Pe=je(at(import.meta.url));function lt(){let e=T(Pe,"../../"),t=T(Pe,"../../..");try{return Ie(T(e,"templates")),e}catch{}try{return Ie(T(t,"templates")),t}catch{}return t}var pt=lt();function se(){let e=T(st.homedir(),".codex"),t=T(e,"config.toml");return{CODEX_HOME:e,CFG:t}}async function le(e){return H.readFile(e,"utf8")}async function Te(e,t){await H.mkdir(je(e),{recursive:!0}),await H.writeFile(e,t,"utf8")}function Ae(e){let t=/^\[profiles\.(.+?)\]/gm,o=[],i;for(;i=t.exec(e);)o.push(i[1]);return o}function pe(e,t){let o=`profile = "${t}"`;if(/^profile\s*=\s*".*"/m.test(e))return e.replace(/^profile\s*=\s*".*"/m,o);let i=e.indexOf(`
19
+ `);return i===-1?o+`
20
+ `+e:e.slice(0,i+1)+o+`
21
+ `+e.slice(i+1)}var Oe=Q({meta:{name:"config",description:"Manage Codex config profiles"},subCommands:{init:Q({meta:{name:"init",description:"Install unified config with multiple profiles"},args:{force:{type:"boolean",description:"Backup and overwrite if exists"}},async run({args:e}){let t=T(pt,"templates/codex-config.toml"),o=await le(t),{CFG:i}=se(),n=await H.access(i).then(()=>!0).catch(()=>!1);if(n&&!e.force){process.stdout.write(`${i} exists. Use --force to overwrite.
22
+ `);return}if(n){let r=`${i}.backup.${Date.now()}`;await H.copyFile(i,r),process.stdout.write(`Backed up to ${r}
23
+ `)}await Te(i,o),process.stdout.write(`Wrote ${i}
24
+ `)}}),profiles:Q({meta:{name:"profiles",description:"List profiles in the current config"},async run(){let{CFG:e}=se(),t=await le(e),o=Ae(t);process.stdout.write(o.length?o.join(`
25
+ `)+`
26
+ `:`No profiles found
27
+ `)}}),"set-profile":Q({meta:{name:"set-profile",description:"Set the active profile in config.toml"},args:{name:{type:"positional",required:!0,description:"Profile name"}},async run({args:e}){let{CFG:t}=se(),o=await le(t),i=Ae(o),n=String(e.name);if(!i.includes(n))throw new Error(`Unknown profile: ${n}`);let r=pe(o,n);await Te(t,r),process.stdout.write(`profile set to ${n}
28
+ `)}})}});var _e=dt(ct(import.meta.url));function ut(){let e=N(_e,"../../"),t=N(_e,"../../..");try{return Fe(N(e,"templates")),e}catch{}try{return Fe(N(t,"templates")),t}catch{}return t}var Z=ut(),Ue=ft({meta:{name:"install",description:"Run the codex-1up installer with validated flags"},args:{yes:{type:"boolean",description:"Non-interactive; accept safe defaults"},"dry-run":{type:"boolean",description:"Print actions without making changes"},"skip-confirmation":{type:"boolean",description:"Skip prompts"},shell:{type:"string",description:"auto|zsh|bash|fish"},vscode:{type:"string",description:"Install VS Code extension id"},"no-vscode":{type:"boolean",description:"Skip VS Code extension checks"},"git-external-diff":{type:"boolean",description:"Set difftastic as git external diff"},"install-node":{type:"string",description:"nvm|brew|skip"},"agents-md":{type:"string",description:"Write starter AGENTS.md to PATH (default PWD/AGENTS.md)",required:!1}},async run({args:e}){let t=N(L.homedir(),".codex","config.toml"),o=await fe(t),i=N(L.homedir(),".codex","notify.sh"),n=await fe(i),r=N(L.homedir(),".codex","AGENTS.md"),s=await fe(r),a,c=!o,R=process.stdout.isTTY&&!e["dry-run"]&&!e["skip-confirmation"]&&!e.yes,$="balanced",w,S,l;if(R){if(p.intro("codex-1up \xB7 Install"),o){let g=await p.confirm({message:"Overwrite existing ~/.codex/config.toml with the latest template? (backup will be created)",initialValue:!0});if(p.isCancel(g))return p.cancel("Install aborted");a=g?"yes":"no",c=g,g||p.log.info("Keeping existing config (no overwrite).")}{let ee=function(b){return[{label:"Skip (leave current setup)",value:"skip"},{label:"None (disable sounds)",value:"none"},...j.map(h=>({label:h,value:h})),{label:"Custom path\u2026",value:"custom"}]};var k=ee;let g=await p.select({message:"Active profile",options:[{label:"balanced (default)",value:"balanced"},{label:"safe",value:"safe"},{label:"minimal",value:"minimal"},{label:"yolo (risky)",value:"yolo"}],initialValue:"balanced"});if(p.isCancel(g))return p.cancel("Install aborted");$=g;let A=N(Z,"sounds"),j=[];try{j=(await U.readdir(A)).filter(b=>/\.(wav|mp3|ogg)$/i.test(b)).sort()}catch{}w="yes";let y=j.includes("noti_1.wav")?"noti_1.wav":j[0]||"none";async function te(b){let h=await p.text({message:"Enter absolute path to a .wav file",placeholder:b||"/absolute/path/to/sound.wav",validate(O){if(!O)return"Path required";if(!O.startsWith("/"))return"Use an absolute path";if(!/(\.wav|\.mp3|\.ogg)$/i.test(O))return"Supported: .wav, .mp3, .ogg"}});if(p.isCancel(h))return null;try{await U.access(String(h))}catch{return p.log.warn("File not found. Try again."),await te(String(h))}return String(h)}let M=await p.select({message:"Notification sound",options:ee(y),initialValue:y});if(p.isCancel(M))return p.cancel("Install aborted");if(M==="skip")w="no",l=void 0;else if(M==="custom"){let b=await te();if(b===null)return p.cancel("Install aborted");y=b}else y=M;if(M!=="skip"){for(;;){let b=await p.select({message:`Selected: ${y}. What next?`,options:[{label:"Preview \u25B6 (press p then Enter)",value:"preview"},{label:"Use this",value:"use"},{label:"Choose another\u2026",value:"change"}],initialValue:"use"});if(p.isCancel(b))return p.cancel("Install aborted");if(b==="use")break;if(b==="change"){let h=await p.select({message:"Notification sound",options:ee(y),initialValue:y});if(p.isCancel(h))return p.cancel("Install aborted");if(h==="custom"){let O=await te();if(O===null)return p.cancel("Install aborted");y=O}else if(h==="skip"){w="no",l=void 0;break}else y=h;continue}try{let h=y==="none"?"none":y.startsWith("/")?y:N(Z,"sounds",y);await gt(h)}catch(h){p.log.warn(String(h))}}l===void 0&&(l=y)}if(s){let b=await p.select({message:"Global ~/.codex/AGENTS.md",options:[{label:"Add to your existing AGENTS.md (Backup will be created)",value:"append-default"},{label:"Overwrite existing (Backup will be created)",value:"overwrite-default"},{label:"Skip \u2014 leave as-is",value:"skip"}],initialValue:"append-default"});if(p.isCancel(b))return p.cancel("Install aborted");S=b}else S="skip"}}let f={profile:$,overwriteConfig:a,notify:w,globalAgents:S,notificationSound:l,mode:"manual",installNode:e["install-node"]||"nvm",shell:String(e.shell||"auto"),vscodeId:e.vscode?String(e.vscode):void 0,noVscode:e["no-vscode"]||!1,agentsMd:typeof e["agents-md"]<"u"?String(e["agents-md"]||process.cwd()):void 0,dryRun:e["dry-run"]||!1,assumeYes:e.yes||!1,skipConfirmation:e["skip-confirmation"]||!1};if(R){let g=p.spinner();g.start("Installing prerequisites and writing config");try{await ae(f,Z),g.stop("Base install complete"),c?await mt($):p.log.info("Profile unchanged (existing config kept)."),p.outro("Install finished")}catch(A){throw g.stop("Installation failed"),p.cancel(`Installation failed: ${A}`),A}await Ge();return}try{R||(a=o?"no":a,c=!1,w=n?"no":"yes",S="skip",f.overwriteConfig=a,f.notify=w,f.globalAgents=S,f.notificationSound=l),await ae(f,Z),await Ge()}catch(g){throw p.cancel(`Installation failed: ${g}`),g}}});async function Ge(){let e=L.homedir(),t=N(e,".codex","config.toml"),o,i=[];try{let c=await U.readFile(t,"utf8"),R=Le.parse(c);o=R.profile;let $=R.profiles||{};i=Object.keys($)}catch{}let n=["codex","ast-grep","fd","rg","fzf","jq","yq","difft","difftastic"],s=(await Promise.all(n.map(async c=>{try{return await G(c),[c,!0]}catch{return[c,!1]}}))).filter(([,c])=>c).map(([c])=>c),a=[];a.push(""),a.push("codex-1up: Installation summary"),a.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),a.push(`Config: ${t}${o?` (active profile: ${o})`:""}`),i.length&&a.push(`Profiles: ${i.join(", ")}`),a.push(`Tools detected: ${s.join(", ")||"none"}`),a.push(""),a.push("Usage:"),a.push(" - Switch profile for a session: codex --profile <name>"),a.push(" - List available profiles: codex-1up config profiles"),a.push(" - Persist active profile: codex-1up config set-profile <name>"),a.push(" - Write AGENTS.md to a repo: codex-1up agents --path . --template default"),a.push(""),process.stdout.write(a.join(`
29
+ `)+`
30
+ `)}async function gt(e){if(e.endsWith("/none")||e==="none")return;let t=[async o=>{await G("afplay"),await x`afplay ${o}`},async o=>{await G("paplay"),await x`paplay ${o}`},async o=>{await G("aplay"),await x`aplay ${o}`},async o=>{await G("mpg123"),await x`mpg123 -q ${o}`},async o=>{await G("ffplay"),await x`ffplay -nodisp -autoexit -loglevel quiet ${o}`}];for(let o of t)try{await o(e);return}catch{}throw new Error("No audio player found (afplay/paplay/aplay/mpg123/ffplay)")}async function mt(e){let t=N(L.homedir(),".codex","config.toml");try{let o=await U.readFile(t,"utf8"),i=pe(o,e);await U.writeFile(t,i,"utf8")}catch{}}async function fe(e){try{return await U.access(e),!0}catch{return!1}}import{defineCommand as wt}from"citty";import{promises as J}from"fs";import{accessSync as ht}from"fs";import{resolve as X,dirname as qe}from"path";import{fileURLToPath as yt}from"url";var Me=qe(yt(import.meta.url));function bt(){let e=X(Me,"../../"),t=X(Me,"../../..");try{return ht(X(e,"templates")),e}catch{}return t}var $t=bt();async function ce(e){try{return await J.access(e),!0}catch{return!1}}async function kt(e,t){if(await ce(t)){let i=`${t}.backup.${new Date().toISOString().replace(/[:.]/g,"").replace("T","_").slice(0,15)}`;await J.copyFile(t,i)}await J.mkdir(qe(t),{recursive:!0}),await J.copyFile(e,t)}var We=wt({meta:{name:"agents",description:"Write an AGENTS.md template"},args:{path:{type:"string",required:!0,description:"Target repo path or file"}},async run({args:e}){let t=String(e.path),o=X($t,"templates/agent-templates","AGENTS-default.md"),n=await ce(t).then(async r=>r&&(await J.stat(t)).isDirectory()).catch(()=>!1)?X(t,"AGENTS.md"):t;if(!await ce(o))throw new Error(`Template not found: ${o}`);await kt(o,n),process.stdout.write(`Wrote ${n}
31
+ `)}});import{defineCommand as vt}from"citty";import{$ as Ct}from"zx";import{fileURLToPath as St}from"url";import{accessSync as Be}from"fs";import{dirname as Rt,resolve as K}from"path";var Ve=Rt(St(import.meta.url));function Et(){let e=K(Ve,"../../"),t=K(Ve,"../../..");try{return Be(K(e,"templates")),e}catch{}try{return Be(K(t,"templates")),t}catch{}return t}var Nt=Et(),ze=vt({meta:{name:"doctor",description:"Run environment checks"},async run(){await Ct`bash ${K(Nt,"scripts/doctor.sh")}`}});import{defineCommand as Dt}from"citty";import{$ as It}from"zx";import{fileURLToPath as Pt}from"url";import{accessSync as He}from"fs";import{dirname as Tt,resolve as Y}from"path";var xe=Tt(Pt(import.meta.url));function At(){let e=Y(xe,"../../"),t=Y(xe,"../../..");try{return He(Y(e,"templates")),e}catch{}try{return He(Y(t,"templates")),t}catch{}return t}var jt=At(),Je=Dt({meta:{name:"uninstall",description:"Clean up aliases and config created by this tool"},async run(){await It`bash ${Y(jt,"scripts/uninstall.sh")}`}});var Xe=Ot({meta:{name:"codex-1up",version:"0.1.0",description:"Power up Codex CLI with clean profiles config and helpers"},subCommands:{install:Ue,agents:We,doctor:ze,uninstall:Je,config:Oe}});Ft(Xe);
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "codex-1up",
3
+ "private": false,
4
+ "type": "module",
5
+ "version": "0.1.1",
6
+ "description": "TypeScript CLI for codex-1up (citty-based)",
7
+ "bin": {
8
+ "codex-1up": "bin/codex-1up.mjs"
9
+ },
10
+ "dependencies": {
11
+ "citty": "^0.1.6",
12
+ "toml": "^3.0.0",
13
+ "@clack/prompts": "^0.11.0",
14
+ "zx": "^8.1.7",
15
+ "fs-extra": "^11.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^24.10.0",
19
+ "@types/fs-extra": "^11.0.4",
20
+ "eslint": "^9.12.0",
21
+ "prettier": "^3.3.3",
22
+ "tsup": "^8.3.0",
23
+ "tsx": "^4.19.2",
24
+ "typescript": "^5.6.3",
25
+ "vitest": "^4.0.8"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "scripts",
30
+ "LICENSE",
31
+ "templates",
32
+ "bin",
33
+ "README.md"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/regenrek/codex-1up.git"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "codex",
45
+ "openai",
46
+ "cli",
47
+ "developer-tools"
48
+ ],
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "scripts": {
53
+ "dev": "tsx src/index.ts",
54
+ "build": "tsup src/main.ts --format esm --dts --minify --clean --out-dir dist",
55
+ "test": "vitest",
56
+ "test:run": "vitest run",
57
+ "coverage": "vitest run --coverage"
58
+ }
59
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ need() { command -v "$1" >/dev/null 2>&1; }
5
+ check() { if need "$1"; then echo "✔ $1"; else echo "✖ $1 (missing)"; fi }
6
+
7
+ echo "codex-1up doctor"
8
+ echo "--- binaries ---"
9
+ for c in node npm codex ast-grep fd fdfind rg fzf jq yq difft delta code; do
10
+ check "$c"
11
+ done
12
+
13
+ echo "--- git ---"
14
+ echo "diff.external = $(git config --global --get diff.external || echo "(none)")"
15
+ echo "difftool.difftastic.cmd = $(git config --global --get difftool.difftastic.cmd || echo "(none)")"
16
+ echo "core.pager = $(git config --global --get core.pager || echo "(none)")"
17
+
18
+ echo "--- codex config ---"
19
+ CFG="$HOME/.codex/config.toml"
20
+ if [ -f "$CFG" ]; then
21
+ echo "found: $CFG"
22
+ if grep -Eiq '^\s*\[tools\]' "$CFG" && grep -Eiq '^\s*web_search\s*=\s*true' "$CFG"; then
23
+ echo "✔ tools.web_search = true"
24
+ else
25
+ echo "✖ tools.web_search not enabled"
26
+ fi
27
+ else
28
+ echo "✖ ~/.codex/config.toml not found"
29
+ fi
30
+
31
+ echo "--- shell rc hints ---"
32
+ echo "SHELL=$SHELL"
33
+ for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.config/fish/config.fish"; do
34
+ [ -f "$rc" ] && grep -q ">>> codex-1up >>>" "$rc" && echo "Found codex-1up block in $rc"
35
+ done
36
+
37
+ echo "Done."
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ payload="${1:-$(cat)}"
3
+
4
+ # Default bundled sound path placeholder; installer should set CODEX_CUSTOM_SOUND.
5
+ DEFAULT_CODEX_SOUND="${HOME}/.codex/sounds/default.wav"
6
+
7
+ # Respect opt-out via env var
8
+ if [ "${CODEX_DISABLE_SOUND:-0}" = "1" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ # Resolve configured sound (absolute path recommended). Allow special value 'none'.
13
+ CODEX_CUSTOM_SOUND="${CODEX_CUSTOM_SOUND:-$DEFAULT_CODEX_SOUND}"
14
+ if [ -z "$CODEX_CUSTOM_SOUND" ] || [ "$CODEX_CUSTOM_SOUND" = "none" ]; then
15
+ exit 0
16
+ fi
17
+
18
+ # Choose an available audio player
19
+ _pick_player() {
20
+ if command -v afplay >/dev/null 2>&1; then echo "afplay"; return 0; fi
21
+ if command -v paplay >/dev/null 2>&1; then echo "paplay"; return 0; fi
22
+ if command -v aplay >/dev/null 2>&1; then echo "aplay"; return 0; fi
23
+ if command -v mpg123 >/dev/null 2>&1; then echo "mpg123"; return 0; fi
24
+ if command -v ffplay >/dev/null 2>&1; then echo "ffplay -nodisp -autoexit"; return 0; fi
25
+ echo ""
26
+ }
27
+
28
+ PLAYER_CMD=$(_pick_player)
29
+ [ -n "$PLAYER_CMD" ] || exit 0
30
+
31
+ # Only attempt to play if file exists
32
+ if [ ! -f "$CODEX_CUSTOM_SOUND" ]; then
33
+ exit 0
34
+ fi
35
+
36
+ _play() {
37
+ # shellcheck disable=SC2086
38
+ $PLAYER_CMD "$CODEX_CUSTOM_SOUND" >/dev/null 2>&1 < /dev/null &
39
+ }
40
+
41
+ if command -v jq >/dev/null 2>&1; then
42
+ notification_type=$(printf '%s' "$payload" | jq -r '.type // empty')
43
+ case "$notification_type" in
44
+ "agent-turn-complete") _play ;;
45
+ *) : ;;
46
+ esac
47
+ else
48
+ _play
49
+ fi
@@ -0,0 +1,198 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ interface PackageTarget {
6
+ name: string;
7
+ dir: string;
8
+ bump?: boolean;
9
+ publish?: boolean;
10
+ access?: "public" | "restricted";
11
+ }
12
+
13
+ const packageTargets: PackageTarget[] = [
14
+ { name: "codex-1up", dir: "cli", bump: true, publish: true, access: "public" },
15
+ ];
16
+
17
+ function run(command: string, cwd: string) {
18
+ console.log(`Executing: ${command} in ${cwd}`);
19
+ execSync(command, { stdio: "inherit", cwd });
20
+ }
21
+
22
+ function ensureCleanWorkingTree() {
23
+ const status = execSync("git status --porcelain", { cwd: "." })
24
+ .toString()
25
+ .trim();
26
+ if (status.length > 0) {
27
+ throw new Error(
28
+ "Working tree has uncommitted changes. Please commit or stash them before running the release script.",
29
+ );
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Bump version in package.json
35
+ * @param pkgPath Path to the package directory
36
+ * @param type Version bump type: 'major', 'minor', 'patch', or specific version
37
+ * @returns The new version
38
+ */
39
+ function bumpVersion(
40
+ pkgPath: string,
41
+ type: "major" | "minor" | "patch" | string,
42
+ ): string {
43
+ const pkgJsonPath = path.join(pkgPath, "package.json");
44
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
45
+ const currentVersion = pkgJson.version;
46
+ let newVersion: string;
47
+
48
+ if (type === "major" || type === "minor" || type === "patch") {
49
+ // Parse current version
50
+ const [major, minor, patch] = currentVersion.split(".").map(Number);
51
+
52
+ // Bump version according to type
53
+ if (type === "major") {
54
+ newVersion = `${major + 1}.0.0`;
55
+ } else if (type === "minor") {
56
+ newVersion = `${major}.${minor + 1}.0`;
57
+ } else {
58
+ // patch
59
+ newVersion = `${major}.${minor}.${patch + 1}`;
60
+ }
61
+ } else {
62
+ // Use the provided version string directly
63
+ newVersion = type;
64
+ }
65
+
66
+ // Update package.json
67
+ pkgJson.version = newVersion;
68
+ fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
69
+
70
+ console.log(
71
+ `Bumped version from ${currentVersion} to ${newVersion} in ${pkgJsonPath}`,
72
+ );
73
+ return newVersion;
74
+ }
75
+
76
+ /**
77
+ * Bump version in all package.json files
78
+ * @param versionBump Version bump type or specific version
79
+ * @returns The new version
80
+ */
81
+ function bumpAllVersions(
82
+ versionBump: "major" | "minor" | "patch" | string = "patch",
83
+ ): string {
84
+ const target = packageTargets[0];
85
+ const pkgPath = path.resolve(target.dir);
86
+ return bumpVersion(pkgPath, versionBump);
87
+ }
88
+
89
+ /**
90
+ * Create a git commit and tag for the release
91
+ * @param version The version to tag
92
+ */
93
+ function createGitCommitAndTag(version: string) {
94
+ console.log("Creating git commit and tag...");
95
+
96
+ try {
97
+ // Stage all changes
98
+ run("git add .", ".");
99
+
100
+ // Create commit with version message
101
+ run(`git commit -m "chore: release v${version}"`, ".");
102
+
103
+ // Create tag
104
+ run(`git tag -a v${version} -m "Release v${version}"`, ".");
105
+
106
+ // Push commit and tag to remote
107
+ console.log("Pushing commit and tag to remote...");
108
+ run("git push", ".");
109
+ run("git push --tags", ".");
110
+
111
+ console.log(`Successfully created and pushed git tag v${version}`);
112
+ } catch (error) {
113
+ console.error("Failed to create git commit and tag:", error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ async function publishPackages(
119
+ versionBump: "major" | "minor" | "patch" | string = "patch",
120
+ ) {
121
+ ensureCleanWorkingTree();
122
+
123
+ const newVersion = bumpAllVersions(versionBump);
124
+
125
+ for (const target of packageTargets.filter((pkg) => pkg.publish)) {
126
+ const pkgPath = path.resolve(target.dir);
127
+ const manifestPath = path.join(pkgPath, "package.json");
128
+ if (!fs.existsSync(manifestPath)) {
129
+ console.warn(`Skipping publish for ${target.name}; missing ${manifestPath}`);
130
+ continue;
131
+ }
132
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
133
+ if (manifest.private) {
134
+ console.warn(
135
+ `Skipping publish for ${target.name}; package.json is marked private`,
136
+ );
137
+ continue;
138
+ }
139
+ // Install deps and build before publish
140
+ // Copy assets from repo root into package (ephemeral for packing only)
141
+ run("rm -rf templates scripts || true", pkgPath);
142
+ run("cp -R ../templates ./templates", pkgPath);
143
+ run("cp -R ../scripts ./scripts", pkgPath);
144
+
145
+ // Ensure README and LICENSE exist inside the package for npm UI
146
+ try {
147
+ const rootReadme = path.resolve(pkgPath, "../README.md");
148
+ if (fs.existsSync(rootReadme)) {
149
+ let readme = fs.readFileSync(rootReadme, "utf8");
150
+ // If README uses local ./public images, rewrite to absolute GitHub raw URLs
151
+ // Derive repo slug from package.json repository.url when possible
152
+ let repoSlug = "";
153
+ try {
154
+ const repoUrl: string | undefined = manifest?.repository?.url;
155
+ if (repoUrl) {
156
+ const m = repoUrl.match(/github\.com\/(.+?)\.git$/);
157
+ if (m) repoSlug = m[1];
158
+ }
159
+ } catch {}
160
+ if (repoSlug) {
161
+ readme = readme.replace(
162
+ /\]\(\.\/public\//g,
163
+ `] (https://raw.githubusercontent.com/${repoSlug}/main/public/`.replace(" ] ", "]"),
164
+ );
165
+ }
166
+ fs.writeFileSync(path.join(pkgPath, "README.md"), readme);
167
+ }
168
+ const rootLicense = path.resolve(pkgPath, "../LICENSE");
169
+ if (fs.existsSync(rootLicense)) {
170
+ fs.copyFileSync(rootLicense, path.join(pkgPath, "LICENSE"));
171
+ }
172
+ } catch (e) {
173
+ console.warn("Failed to prepare README/LICENSE in package:", e);
174
+ }
175
+
176
+ run("pnpm i --frozen-lockfile=false", pkgPath);
177
+ run("pnpm build", pkgPath);
178
+ const accessFlag = target.access === "public" ? " --access public" : "";
179
+ console.log(`Publishing ${target.name}@${newVersion}...`);
180
+ run(`pnpm publish --no-git-checks${accessFlag}`, pkgPath);
181
+
182
+ // Clean up ephemeral copies so repo doesn't keep duplicates
183
+ try {
184
+ fs.rmSync(path.join(pkgPath, "templates"), { recursive: true, force: true });
185
+ fs.rmSync(path.join(pkgPath, "scripts"), { recursive: true, force: true });
186
+ fs.rmSync(path.join(pkgPath, "README.md"), { force: true });
187
+ fs.rmSync(path.join(pkgPath, "LICENSE"), { force: true });
188
+ } catch {}
189
+ }
190
+
191
+ createGitCommitAndTag(newVersion);
192
+ }
193
+
194
+ // Get version bump type from command line arguments
195
+ const args = process.argv.slice(2);
196
+ const versionBumpArg = args[0] || "patch"; // Default to patch
197
+
198
+ publishPackages(versionBumpArg).catch(console.error);
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT="codex-1up"
5
+ echo "Uninstall: removing shell alias blocks and git config entries"
6
+
7
+ remove_block() {
8
+ local rc="$1"
9
+ [ -f "$rc" ] || return 0
10
+ if grep -q ">>> ${PROJECT} >>>" "$rc"; then
11
+ # shellcheck disable=SC2016
12
+ sed -i.bak -e "/>>> ${PROJECT} >>>/,/<<< ${PROJECT} <</d" "$rc"
13
+ echo "Cleaned ${rc} (backup at ${rc}.bak)"
14
+ fi
15
+ }
16
+
17
+ remove_block "$HOME/.zshrc"
18
+ remove_block "$HOME/.bashrc"
19
+ remove_block "$HOME/.config/fish/config.fish"
20
+
21
+ # Git settings (safe to leave, but we can remove)
22
+ if [ "$(git config --global --get difftool.difftastic.cmd)" ]; then
23
+ git config --global --unset difftool.difftastic.cmd || true
24
+ git config --global --unset difftool.prompt || true
25
+ echo "Removed git difftastic difftool config"
26
+ fi
27
+
28
+ if [ "$(git config --global --get diff.external)" = "difft" ]; then
29
+ git config --global --unset diff.external || true
30
+ echo "Removed git diff.external=difft"
31
+ fi
32
+
33
+ echo "Note: packages (codex, fd, rg, ast-grep, difftastic, etc.) are not removed."
34
+ echo "Uninstall complete."
@@ -0,0 +1,22 @@
1
+ # AGENTS.md — Tool Selection
2
+
3
+ When you need to call tools from the shell, use this rubric:
4
+
5
+ ## File Operations
6
+ - Use `fd` for finding files: `fd --full-path '<pattern>' | head -n 1`
7
+
8
+ ## Structured Code Search
9
+ - Find code structure: `ast-grep --lang <language> -p '<pattern>'`
10
+ - List matching files: `ast-grep -l --lang <language> -p '<pattern>' | head -n 10`
11
+ - Prefer `ast-grep` over `rg`/`grep` when you need syntax-aware matching
12
+
13
+ ## Data Processing
14
+ - JSON: `jq`
15
+ - YAML/XML: `yq`
16
+
17
+ ## Selection
18
+ - Select from multiple results deterministically (non-interactive filtering)
19
+ - Fuzzy finder: `fzf --filter 'term' | head -n 1`
20
+
21
+ ## Guidelines
22
+ - Prefer deterministic, non-interactive commands (`head`, `--filter`, `--json` + `jq`) so runs are reproducible
@@ -0,0 +1,57 @@
1
+ # ~/.codex/config.toml — created by codex-1up
2
+ # Adjust to your liking. See Codex CLI docs for full options.
3
+
4
+ # Core (root defaults)
5
+ model = "gpt-5"
6
+ approval_policy = "on-request" # untrusted|on-failure|on-request|never
7
+ sandbox_mode = "workspace-write" # read-only|workspace-write|danger-full-access
8
+ profile = "balanced" # active profile: balanced|safe|minimal|yolo
9
+
10
+ # Extra settings that only apply when `sandbox = "workspace-write"`.
11
+ [sandbox_workspace_write]
12
+ # Allow the command being run inside the sandbox to make outbound network
13
+ # requests. Disabled by default.
14
+ network_access = true
15
+
16
+ # UI & notifications (can be toggled by installer)
17
+ [tui]
18
+ # Desktop notifications from the TUI: boolean or filtered list. Default: false
19
+ # Examples: true | ["agent-turn-complete", "approval-requested"]
20
+ notifications = false
21
+
22
+ # Centralized feature flags — booleans only
23
+ [features]
24
+ web_search_request = true
25
+ unified_exec = false
26
+ streamable_shell = false
27
+ rmcp_client = false
28
+ apply_patch_freeform = false
29
+ view_image_tool = true
30
+ experimental_sandbox_command_assessment = false
31
+ ghost_commit = false
32
+ enable_experimental_windows_sandbox = false
33
+
34
+ # ---- Profiles (override only what differs from root) -----------------------
35
+
36
+ [profiles.balanced]
37
+ approval_policy = "on-request"
38
+ sandbox_mode = "workspace-write"
39
+ [profiles.balanced.features]
40
+ web_search_request = true
41
+
42
+ [profiles.safe]
43
+ approval_policy = "on-failure"
44
+ sandbox_mode = "workspace-write"
45
+ [profiles.safe.features]
46
+ web_search_request = false
47
+
48
+ [profiles.minimal]
49
+ model_reasoning_effort = "minimal"
50
+ [profiles.minimal.features]
51
+ web_search_request = false
52
+
53
+ [profiles.yolo]
54
+ approval_policy = "never"
55
+ sandbox_mode = "danger-full-access"
56
+ [profiles.yolo.features]
57
+ web_search_request = true
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Codex notification hook — installed to ~/.codex/notify.sh by codex-1up
3
+ # Reads JSON payload on stdin or as first arg. Plays a short sound for specific events.
4
+
5
+ set -euo pipefail
6
+
7
+ payload="${1:-$(cat)}"
8
+
9
+ # Default bundled sound path; can be overridden via CODEX_CUSTOM_SOUND in your shell rc.
10
+ DEFAULT_CODEX_SOUND="${HOME}/.codex/sounds/default.wav"
11
+
12
+ # Respect opt-out via env var
13
+ if [ "${CODEX_DISABLE_SOUND:-0}" = "1" ]; then
14
+ exit 0
15
+ fi
16
+
17
+ # Resolve configured sound (absolute path recommended). Allow special value 'none'.
18
+ CODEX_CUSTOM_SOUND="${CODEX_CUSTOM_SOUND:-$DEFAULT_CODEX_SOUND}"
19
+ if [ -z "$CODEX_CUSTOM_SOUND" ] || [ "$CODEX_CUSTOM_SOUND" = "none" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ # Choose an available audio player
24
+ _pick_player() {
25
+ if command -v afplay >/dev/null 2>&1; then echo "afplay"; return 0; fi
26
+ if command -v paplay >/dev/null 2>&1; then echo "paplay"; return 0; fi
27
+ if command -v aplay >/dev/null 2>&1; then echo "aplay"; return 0; fi
28
+ if command -v mpg123 >/dev/null 2>&1; then echo "mpg123"; return 0; fi
29
+ if command -v ffplay >/dev/null 2>&1; then echo "ffplay -nodisp -autoexit"; return 0; fi
30
+ echo ""
31
+ }
32
+
33
+ PLAYER_CMD=$(_pick_player)
34
+ [ -n "$PLAYER_CMD" ] || exit 0
35
+
36
+ # Only attempt to play if file exists
37
+ if [ ! -f "$CODEX_CUSTOM_SOUND" ]; then
38
+ exit 0
39
+ fi
40
+
41
+ _play() {
42
+ # shellcheck disable=SC2086
43
+ $PLAYER_CMD "$CODEX_CUSTOM_SOUND" >/dev/null 2>&1 < /dev/null &
44
+ }
45
+
46
+ if command -v jq >/dev/null 2>&1; then
47
+ notification_type=$(printf '%s' "$payload" | jq -r '.type // empty')
48
+ case "$notification_type" in
49
+ "agent-turn-complete") _play ;;
50
+ *) : ;;
51
+ esac
52
+ else
53
+ _play
54
+ fi