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 +21 -0
- package/README.md +131 -0
- package/bin/codex-1up.mjs +20 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +31 -0
- package/package.json +59 -0
- package/scripts/doctor.sh +37 -0
- package/scripts/notification.sh +49 -0
- package/scripts/release.ts +198 -0
- package/scripts/uninstall.sh +34 -0
- package/templates/agent-templates/AGENTS-default.md +22 -0
- package/templates/codex-config.toml +57 -0
- package/templates/notification.sh +54 -0
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
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
|