(opts: {
skillDir: string; // Path to skill directory containing SKILL.md
prompt: string; // What to ask Codex to do with the skill
timeoutMs?: number; // Default 300000 (5 min)
cwd?: string; // Working directory
skillName?: string; // Skill name for installation (default: dirname)
sandbox?: string; // Sandbox mode (default: 'read-only')
})
| 138 | * and returns a CodexResult. Skips gracefully if codex binary is not found. |
| 139 | */ |
| 140 | export async function runCodexSkill(opts: { |
| 141 | skillDir: string; // Path to skill directory containing SKILL.md |
| 142 | prompt: string; // What to ask Codex to do with the skill |
| 143 | timeoutMs?: number; // Default 300000 (5 min) |
| 144 | cwd?: string; // Working directory |
| 145 | skillName?: string; // Skill name for installation (default: dirname) |
| 146 | sandbox?: string; // Sandbox mode (default: 'read-only') |
| 147 | }): Promise<CodexResult> { |
| 148 | const { |
| 149 | skillDir, |
| 150 | prompt, |
| 151 | timeoutMs = 300_000, |
| 152 | cwd, |
| 153 | skillName, |
| 154 | sandbox = 'read-only', |
| 155 | } = opts; |
| 156 | |
| 157 | const startTime = Date.now(); |
| 158 | const name = skillName || path.basename(skillDir) || 'gstack'; |
| 159 | |
| 160 | // Check if codex binary exists |
| 161 | const whichResult = Bun.spawnSync(['which', 'codex']); |
| 162 | if (whichResult.exitCode !== 0) { |
| 163 | return { |
| 164 | output: 'SKIP: codex binary not found', |
| 165 | reasoning: [], |
| 166 | toolCalls: [], |
| 167 | tokens: 0, |
| 168 | exitCode: -1, |
| 169 | durationMs: Date.now() - startTime, |
| 170 | sessionId: null, |
| 171 | rawLines: [], |
| 172 | stderr: '', |
| 173 | }; |
| 174 | } |
| 175 | |
| 176 | // Set up temp HOME with skill installed |
| 177 | const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-e2e-')); |
| 178 | const realHome = os.homedir(); |
| 179 | |
| 180 | try { |
| 181 | installSkillToTempHome(skillDir, name, tempHome); |
| 182 | |
| 183 | // Symlink real Codex auth config so codex can authenticate from temp HOME. |
| 184 | // Codex stores auth in ~/.codex/ — we need the config but not the skills |
| 185 | // (we install our own test skills above). |
| 186 | const realCodexConfig = path.join(realHome, '.codex'); |
| 187 | const tempCodexDir = path.join(tempHome, '.codex'); |
| 188 | if (fs.existsSync(realCodexConfig)) { |
| 189 | // Copy auth-related files from real ~/.codex/ into temp ~/.codex/ |
| 190 | // (skills/ is already set up by installSkillToTempHome) |
| 191 | const entries = fs.readdirSync(realCodexConfig); |
| 192 | for (const entry of entries) { |
| 193 | if (entry === 'skills') continue; // don't clobber our test skills |
| 194 | const src = path.join(realCodexConfig, entry); |
| 195 | const dst = path.join(tempCodexDir, entry); |
| 196 | if (!fs.existsSync(dst)) { |
| 197 | fs.cpSync(src, dst, { recursive: true }); |
no test coverage detected