diff --git a/README.md b/README.md index cb5e1dc..9699731 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ To use the interactive mode, you'll also need one of the following LLM CLI tools - **GitHub Copilot CLI** — Install the [GitHub CLI](https://cli.github.com/), authenticate with `gh auth login`, ensure Copilot access is enabled for your account/organization, then run `gh extension install github/gh-copilot` - **Claude Code** — [Install Claude Code](https://docs.anthropic.com/en/docs/claude-code) +- **OpenAI Codex CLI** — [Install Codex CLI](https://github.com/openai/codex) Not using a CLI tool? See [Using with any LLM (manual)](#using-with-any-llm-manual). @@ -179,6 +180,15 @@ cd promptkit claude "Read and execute bootstrap.md" ``` +### Using with Codex CLI + +Codex also supports reading the bootstrap file directly from the repo root: + +```bash +cd promptkit +codex "Read and execute bootstrap.md" +``` + ### Using with any LLM (manual) If your tool doesn't support skills or file access, paste the bootstrap diff --git a/cli/bin/cli.js b/cli/bin/cli.js index 4c9a778..9e64730 100644 --- a/cli/bin/cli.js +++ b/cli/bin/cli.js @@ -55,7 +55,7 @@ program .description("Launch an interactive session with your LLM CLI (default)") .option( "--cli ", - "LLM CLI to use (copilot, gh-copilot, claude)" + "LLM CLI to use (copilot, gh-copilot, claude, codex)" ) .option( "--dry-run", diff --git a/cli/lib/launch.js b/cli/lib/launch.js index 037b549..8461d52 100644 --- a/cli/lib/launch.js +++ b/cli/lib/launch.js @@ -8,18 +8,37 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); +function pathDirs() { + return (process.env.PATH || "").split(path.delimiter).filter(Boolean); +} + +function windowsPathExts() { + return (process.env.PATHEXT || ".EXE;.COM;.BAT;.CMD") + .split(";") + .map((e) => e.toLowerCase()); +} + +function isExactFileOnPath(fileName) { + for (const dir of pathDirs()) { + try { + fs.accessSync(path.join(dir, fileName), fs.constants.F_OK); + return true; + } catch { + // not found in this directory, continue + } + } + return false; +} + function isOnPath(cmd) { // Search PATH entries directly rather than shelling out to `which`/`where`. // This avoids requiring `which` to be on PATH itself (important in test // environments where PATH is restricted to a mock directory). - const pathDirs = (process.env.PATH || "").split(path.delimiter).filter(Boolean); - const exts = process.platform === "win32" - ? (process.env.PATHEXT || ".EXE;.COM;.BAT;.CMD").split(";").map((e) => e.toLowerCase()) - : [""]; + const exts = process.platform === "win32" ? windowsPathExts() : [""]; // On Windows, X_OK is not meaningful — any file with a matching PATHEXT // extension is considered executable, so we check for existence (F_OK) only. const accessFlag = process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK; - for (const dir of pathDirs) { + for (const dir of pathDirs()) { for (const ext of exts) { try { fs.accessSync(path.join(dir, cmd + ext), accessFlag); @@ -32,6 +51,31 @@ function isOnPath(cmd) { return false; } +function resolveSpawnCommand(cmd) { + if (process.platform !== "win32") return cmd; + + const shim = `${cmd}.cmd`; + return isExactFileOnPath(shim) ? shim : cmd; +} + +function quoteWindowsArg(arg) { + if (arg === "") return '""'; + if (!/[\s"]/u.test(arg)) return arg; + return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`; +} + +function spawnCli(cmd, args, options) { + if (process.platform === "win32" && /\.cmd$/i.test(cmd)) { + const comspec = process.env.ComSpec || "cmd.exe"; + const commandLine = [cmd, ...args].map(quoteWindowsArg).join(" "); + return spawn(comspec, ["/d", "/s", "/c", commandLine], { + ...options, + windowsVerbatimArguments: true, + }); + } + return spawn(cmd, args, options); +} + function detectCli() { // Check for GitHub Copilot CLI first (most common) if (isOnPath("copilot")) return "copilot"; @@ -45,6 +89,7 @@ function detectCli() { } } if (isOnPath("claude")) return "claude"; + if (isOnPath("codex")) return "codex"; return null; } @@ -76,7 +121,8 @@ function launchInteractive(contentDir, cliName, { dryRun = false } = {}) { "No supported LLM CLI found on PATH.\n\n" + "Install one of:\n" + " - GitHub Copilot CLI: gh extension install github/gh-copilot\n" + - " - Claude Code: https://docs.anthropic.com/en/docs/claude-code\n\n" + + " - Claude Code: https://docs.anthropic.com/en/docs/claude-code\n" + + " - OpenAI Codex CLI: https://github.com/openai/codex\n\n" + "Alternatively, load bootstrap.md in your LLM manually from:\n" + ` ${contentDir}` ); @@ -107,7 +153,7 @@ function launchInteractive(contentDir, cliName, { dryRun = false } = {}) { let cmd, args; switch (cli) { case "copilot": - cmd = "copilot"; + cmd = resolveSpawnCommand("copilot"); // --add-dir grants file access to the staging directory. args = ["--add-dir", tmpDir, "-i", bootstrapPrompt]; break; @@ -117,7 +163,11 @@ function launchInteractive(contentDir, cliName, { dryRun = false } = {}) { break; case "claude": // --add-dir grants file access to the staging directory. - cmd = "claude"; + cmd = resolveSpawnCommand("claude"); + args = ["--add-dir", tmpDir, bootstrapPrompt]; + break; + case "codex": + cmd = resolveSpawnCommand("codex"); args = ["--add-dir", tmpDir, bootstrapPrompt]; break; default: @@ -142,7 +192,7 @@ function launchInteractive(contentDir, cliName, { dryRun = false } = {}) { // All CLIs are spawned from the user's original directory so the LLM // session reflects the directory the user was working in. - const child = spawn(cmd, args, { + const child = spawnCli(cmd, args, { cwd: originalCwd, stdio: "inherit", }); diff --git a/cli/package.json b/cli/package.json index 640dfc5..0ddfe91 100644 --- a/cli/package.json +++ b/cli/package.json @@ -37,6 +37,7 @@ "llm", "ai", "copilot", + "codex", "prompt-templates", "agentic-ai", "developer-tools" diff --git a/cli/specs/design.md b/cli/specs/design.md index e9ff5be..98dff3a 100644 --- a/cli/specs/design.md +++ b/cli/specs/design.md @@ -108,7 +108,7 @@ validate content availability. by category, and displays the result. No separate `manifest.js` module is used (see REQ-CLI-103). - The `--cli` flag documents valid values (`copilot`, `gh-copilot`, - `claude`) in its help text (see REQ-CLI-011). + `claude`, `codex`) in its help text (see REQ-CLI-011). **Key function**: @@ -146,10 +146,15 @@ interactive session. - CLI detection uses `execFileSync` with `where` (Windows) or `which` (Unix) — this is the most reliable cross-platform way to check if a command exists on PATH without actually executing it. -- The detection order (copilot → gh-copilot → claude) prioritizes GitHub +- The detection order (copilot → gh-copilot → claude → codex) prioritizes GitHub Copilot CLI as the primary target. The `gh copilot` variant is checked by actually running `gh copilot --help` to verify the extension is installed, not just that `gh` exists. +- On Windows, npm-installed CLIs such as `copilot`, `claude`, and `codex` + may need their `.cmd` shims invoked explicitly because Node's + `child_process.spawn()` does not resolve commands the same way an + interactive shell does. The launcher therefore prefers `.cmd` + when present on `PATH`. - Content is copied to a temp directory (`os.tmpdir()` + `mkdtempSync`) because LLM CLIs need to read the files from their CWD, and the npm package's `content/` directory may be in a read-only or non-obvious @@ -176,7 +181,7 @@ Internal helper. Checks if a command exists on PATH using platform- appropriate lookup. ``` -detectCli() → "copilot" | "gh-copilot" | "claude" | null +detectCli() → "copilot" | "gh-copilot" | "claude" | "codex" | null ``` Probes PATH for supported LLM CLIs in priority order. @@ -397,7 +402,7 @@ Global options: Interactive options: --cli Override LLM CLI auto-detection - Valid values: copilot, gh-copilot, claude + Valid values: copilot, gh-copilot, claude, codex ``` ### 5.2 Module Exports @@ -405,7 +410,7 @@ Interactive options: **launch.js**: ```javascript module.exports = { - detectCli, // () → "copilot" | "gh-copilot" | "claude" | null + detectCli, // () → "copilot" | "gh-copilot" | "claude" | "codex" | null launchInteractive, // (contentDir: string, cliName: string | null) → never copyContentToTemp // (contentDir: string) → string (tmpDir path) } diff --git a/cli/specs/requirements.md b/cli/specs/requirements.md index 0ae456b..cb735ee 100644 --- a/cli/specs/requirements.md +++ b/cli/specs/requirements.md @@ -1,5 +1,5 @@ --- -title: "PromptKit CLI — Requirements Specification" +title: "PromptKit CLI ??? Requirements Specification" project: "PromptKit CLI (@alan-jowett/promptkit)" version: "0.6.1" date: "2026-03-31" @@ -11,14 +11,14 @@ source_files: - cli/package.json --- -# PromptKit CLI — Requirements Specification +# PromptKit CLI ??? Requirements Specification ## Revision History | Rev | Date | Author | Description | |-----|------|--------|-------------| | 0.1 | 2025-07-17 | Spec-extraction-workflow | Initial draft extracted from source code | -| 0.2 | 2025-07-18 | Engineering-workflow Phase 2 | Retired assemble command (REQ-CLI-030–037), assembly engine (REQ-CLI-040–051), manifest resolution module (REQ-CLI-060–069). Kept list command with inline manifest parsing. Modified REQ-CLI-002, 004, 011, 012, 020–023, 080, 091, 094. Retired REQ-CLI-092, CON-005, ASSUMPTION-002, ASSUMPTION-006. Added REQ-CLI-100, 101, 103. | +| 0.2 | 2025-07-18 | Engineering-workflow Phase 2 | Retired assemble command (REQ-CLI-030???037), assembly engine (REQ-CLI-040???051), manifest resolution module (REQ-CLI-060???069). Kept list command with inline manifest parsing. Modified REQ-CLI-002, 004, 011, 012, 020???023, 080, 091, 094. Retired REQ-CLI-092, CON-005, ASSUMPTION-002, ASSUMPTION-006. Added REQ-CLI-100, 101, 103. | | 0.3 | 2026-03-31 | Bug-fix | Added REQ-CLI-024 (cwd preservation for claude). Updated REQ-CLI-015 and REQ-CLI-017 to reflect per-CLI spawn cwd behaviour. | | 0.4 | 2026-03-31 | Bug-fix | Extended cwd fix to all CLIs. Added REQ-CLI-025 (--add-dir for staging directory). Updated REQ-CLI-015, 016, 017, 024 to be CLI-agnostic. | @@ -31,9 +31,9 @@ source_files: The PromptKit CLI is a Node.js command-line tool (`@alan-jowett/promptkit`) that provides two capabilities: -1. **Interactive launch** — detect an LLM CLI on PATH, stage PromptKit +1. **Interactive launch** ??? detect an LLM CLI on PATH, stage PromptKit content, and spawn the LLM with the bootstrap prompt. -2. **Template listing** — enumerate available prompt templates from +2. **Template listing** ??? enumerate available prompt templates from `manifest.yaml` for discovery. The CLI also includes a **build-time content bundling** script that copies @@ -41,10 +41,10 @@ PromptKit library content from the repository root into the npm package. ### 1.2 What the CLI Is NOT -- The CLI is NOT an LLM or AI tool — it launches external LLM CLIs. -- The CLI does NOT interpret or execute prompts — it stages content and +- The CLI is NOT an LLM or AI tool ??? it launches external LLM CLIs. +- The CLI does NOT interpret or execute prompts ??? it stages content and delegates prompt assembly to the LLM via `bootstrap.md`. -- The CLI does NOT assemble prompts programmatically — all prompt assembly +- The CLI does NOT assemble prompts programmatically ??? all prompt assembly is performed by the LLM when following `bootstrap.md`. --- @@ -79,14 +79,14 @@ executing any command, and exit with code 1 and an error message if ### 2.2 Interactive Command **REQ-CLI-010**: The `interactive` command MUST detect a supported LLM CLI -on the system PATH using the detection order: `copilot` → `gh copilot` → -`claude`. -- *Source*: `launch.js` lines 21–35 (`detectCli()`). +on the system PATH using the detection order: `copilot` -> `gh copilot` -> +`claude` -> `codex`. +- *Source*: `launch.js` (`detectCli()`). - *Acceptance*: With only `claude` on PATH, `detectCli()` returns `"claude"`. **REQ-CLI-011**: The `interactive` command MUST accept an optional `--cli ` flag to override auto-detection. Valid values (`copilot`, -`gh-copilot`, `claude`) SHOULD be documented in `--help` output. +`gh-copilot`, `claude`, `codex`) SHOULD be documented in `--help` output. - *Source*: `cli.js`. - *Acceptance*: `promptkit interactive --cli claude` uses `claude` regardless of what is detected. `promptkit interactive --help` lists valid `--cli` values. @@ -103,12 +103,12 @@ directory and loading `bootstrap.md` manually as an alternative. **REQ-CLI-013**: If auto-detection selects a CLI other than `copilot` or `gh-copilot`, and the user did not pass `--cli`, the CLI SHOULD print a warning indicating the fallback CLI being used. -- *Source*: `launch.js` lines 73–79. +- *Source*: `launch.js` lines 73???79. - *Acceptance*: When only `claude` is detected, a warning mentions fallback. **REQ-CLI-014**: The `interactive` command MUST copy the entire content directory to a temporary directory before launching the LLM CLI. -- *Source*: `launch.js` lines 37–41 (`copyContentToTemp()`). +- *Source*: `launch.js` lines 37???41 (`copyContentToTemp()`). - *Acceptance*: A temp directory under the OS temp path contains all content files. @@ -136,6 +136,7 @@ absolute path to `bootstrap.md`: - `copilot`: `copilot --add-dir -i "Read and execute /bootstrap.md"` - `gh-copilot`: `gh copilot --add-dir -i "Read and execute /bootstrap.md"` - `claude`: `claude --add-dir "Read and execute /bootstrap.md"` +- `codex`: `codex --add-dir "Read and execute /bootstrap.md"` - *Source*: `launch.js` (`launchInteractive()`). - *Acceptance*: Spawn is called with the documented cmd/args for each CLI. @@ -161,14 +162,14 @@ cwd is the user's original working directory. temporary directory (best-effort) and then exit with the child's exit code. If the child was killed by a signal, the CLI MUST re-send that signal to its own process. -- *Source*: `launch.js` lines 122–133. +- *Source*: `launch.js` lines 122???133. - *Acceptance*: After the child exits, the temp directory is removed and `process.exitCode` matches. **REQ-CLI-019**: If spawning the child process fails (error event), the CLI MUST print an error message, attempt to clean up the temp directory, and exit with code 1. -- *Source*: `launch.js` lines 112–119. +- *Source*: `launch.js` lines 112???119. - *Acceptance*: With an invalid CLI name, `promptkit --cli nonexistent` prints an error and exits 1. @@ -251,8 +252,8 @@ inclusion.~~ comments, not just the first one, handling consecutive comment blocks.~~ **[RETIRED] REQ-CLI-043**: ~~The assembly engine MUST concatenate components -in a fixed section order: Identity → Reasoning Protocols → Classification -Taxonomy → Output Format → Task.~~ +in a fixed section order: Identity ??? Reasoning Protocols ??? Classification +Taxonomy ??? Output Format ??? Task.~~ **[RETIRED] REQ-CLI-044**: ~~Sections MUST be separated by `\n\n---\n\n` (horizontal rule with blank lines).~~ @@ -333,7 +334,7 @@ directories from the repository root to `cli/content/`: `personas`, **REQ-CLI-071**: The `copy-content.js` script MUST copy the following individual files from the repository root to `cli/content/`: `manifest.yaml`, `bootstrap.md`. -- *Source*: `copy-content.js` lines 15, 46–50. +- *Source*: `copy-content.js` lines 15, 46???50. - *Acceptance*: After running, `cli/content/manifest.yaml` and `cli/content/bootstrap.md` exist. @@ -344,23 +345,23 @@ with `.md` or `.yaml` extensions, skipping all other file types. **REQ-CLI-073**: The script MUST delete and recreate the `cli/content/` directory before copying to ensure a clean state. -- *Source*: `copy-content.js` lines 40–43. +- *Source*: `copy-content.js` lines 40???43. - *Acceptance*: Stale files from a previous copy are removed. **REQ-CLI-074**: The script MUST validate that `manifest.yaml` exists at the repository root before proceeding, and exit with code 1 if not found. -- *Source*: `copy-content.js` lines 32–37. +- *Source*: `copy-content.js` lines 32???37. - *Acceptance*: Running from outside the repo prints an error and exits 1. **REQ-CLI-075**: The script MUST print a summary of how many entries were copied upon completion. -- *Source*: `copy-content.js` lines 69–70. +- *Source*: `copy-content.js` lines 69???70. - *Acceptance*: Output includes `"Copied PromptKit content to cli/content/"`. **REQ-CLI-076**: The `copy-content.js` script MUST run automatically on `npm publish` (via `prepublishOnly`) and `npm install` from git (via `prepare`). -- *Source*: `package.json` lines 20–21. +- *Source*: `package.json` lines 20???21. - *Acceptance*: `npm pack` triggers the script; `prepare` hook runs on `npm install` from the repository. @@ -369,7 +370,7 @@ copied upon completion. **REQ-CLI-080**: The npm package MUST include only `bin/`, `lib/`, and `content/` directories (plus package.json). The `lib/` directory MUST contain only `launch.js`. -- *Source*: `package.json` lines 14–18 (`files` field). +- *Source*: `package.json` lines 14???18 (`files` field). - *Acceptance*: `npm pack --dry-run` lists only files under those directories. `lib/` contains only `launch.js`. @@ -380,7 +381,7 @@ generated at build time. **REQ-CLI-082**: The package MUST be published with public access under the `@alan-jowett` scope. -- *Source*: `package.json` lines 30–32 (`publishConfig`). +- *Source*: `package.json` lines 30???32 (`publishConfig`). - *Acceptance*: `npm publish` uses `access: public`. --- @@ -416,7 +417,7 @@ components when following `bootstrap.md`.* **REQ-CLI-094**: The CLI MUST have exactly two runtime dependencies: `commander` (^12.0.0) and `js-yaml` (^4.1.0). All other modules used MUST be Node.js built-ins. -- *Source*: `package.json` lines 23–26. +- *Source*: `package.json` lines 23???26. - *Acceptance*: `package.json` lists exactly these two dependencies. --- @@ -436,7 +437,7 @@ any secrets. LLM authentication is handled by the external CLI. is stateless. **[RETIRED] CON-005**: ~~The assembly engine MUST NOT summarize, abbreviate, -or condense component content — it includes body text verbatim (minus +or condense component content ??? it includes body text verbatim (minus frontmatter/comments).~~ *Retired: Assembly engine removed. The equivalent rule exists in @@ -461,11 +462,11 @@ template entries directly.* **[ASSUMPTION-003]**: The `--cli` flag for the `interactive` command accepts values matching the switch cases in `launch.js`: `"copilot"`, -`"gh-copilot"`, `"claude"`. Other values cause exit with code 1. These +`"gh-copilot"`, `"claude"`, `"codex"`. Other values cause exit with code 1. These valid values SHOULD be documented in help text (see REQ-CLI-011). **[ASSUMPTION-004]**: The `copyContentToTemp` function copies the entire -content directory recursively, including all file types — unlike +content directory recursively, including all file types ??? unlike `copy-content.js` which filters to `.md` and `.yaml` only. This means the temp directory may contain files not present in the npm package if run from a development environment. [INFERRED] @@ -480,7 +481,7 @@ This relies on standard npm lifecycle behavior. very beginning of the file (after HTML comment stripping). Content files with frontmatter not at the start will not be stripped. [INFERRED]~~ -*Retired: Assembly engine removed — frontmatter stripping no longer +*Retired: Assembly engine removed ??? frontmatter stripping no longer applies to CLI code.* --- diff --git a/cli/specs/validation.md b/cli/specs/validation.md index 483bed0..97176b1 100644 --- a/cli/specs/validation.md +++ b/cli/specs/validation.md @@ -221,6 +221,12 @@ See REQ-CLI-100.* - *Steps*: Ensure only `claude` is on PATH. - *Expected*: Returns `"claude"`. +**TC-CLI-072A**: detectCli finds codex after claude is absent. +- *Requirement*: REQ-CLI-010 +- *Type*: Unit +- *Steps*: Ensure only `codex` is on PATH. +- *Expected*: Returns `"codex"`. + **TC-CLI-073**: detectCli returns null when nothing found. - *Requirement*: REQ-CLI-010 - *Type*: Unit @@ -275,7 +281,7 @@ See REQ-CLI-100.* **TC-CLI-081**: Correct command construction for each CLI. - *Requirement*: REQ-CLI-017 - *Type*: Unit -- *Steps*: Verify spawn cmd/args for `copilot`, `gh-copilot`, `claude`. +- *Steps*: Verify spawn cmd/args for `copilot`, `gh-copilot`, `claude`, `codex`. - *Expected*: - copilot: `cmd="copilot"`, args include `"--add-dir"`, ``, `"-i"`, `"Read and execute /bootstrap.md"` @@ -283,6 +289,8 @@ See REQ-CLI-100.* ``, `"-i"`, `"Read and execute /bootstrap.md"` - claude: `cmd="claude"`, args include `"--add-dir"`, ``, `"Read and execute /bootstrap.md"` + - codex: `cmd="codex"`, args include `"--add-dir"`, ``, + `"Read and execute /bootstrap.md"` **TC-CLI-082**: All CLIs are spawned with the user's original working directory. - *Requirement*: REQ-CLI-024 @@ -422,7 +430,7 @@ concern.* | REQ-CLI-002 | TC-CLI-001, TC-CLI-004 | High | Active | | REQ-CLI-003 | TC-CLI-002 | Medium | Active | | REQ-CLI-004 | TC-CLI-003, TC-CLI-003a | High | Active | -| REQ-CLI-010 | TC-CLI-070 through TC-CLI-074 | High | Active | +| REQ-CLI-010 | TC-CLI-070 through TC-CLI-074, TC-CLI-072A | High | Active | | REQ-CLI-011 | TC-CLI-075 | Medium | Active | | REQ-CLI-012 | TC-CLI-076 | High | Active | | REQ-CLI-013 | TC-CLI-077 | Low | Active | diff --git a/cli/tests/launch.test.js b/cli/tests/launch.test.js index c584525..19b9856 100644 --- a/cli/tests/launch.test.js +++ b/cli/tests/launch.test.js @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// cli/tests/launch.test.js — Launch module unit tests +// cli/tests/launch.test.js ??? Launch module unit tests const { describe, it, before, after } = require("node:test"); const assert = require("node:assert"); @@ -46,7 +46,7 @@ describe("Launch Module", () => { before(() => { assert.ok( fs.existsSync(contentDir), - "content/ must exist — run 'npm run prepare' first" + "content/ must exist ??? run 'npm run prepare' first" ); }); @@ -110,8 +110,8 @@ describe("Launch Module", () => { // Run an inline Node script that requires launch.js by absolute path // and calls detectCli() with PATH set to mockDir only. - // isOnPath() in launch.js searches PATH directories directly (no `which`), - // so mockDir is sufficient — no system binary directories are needed. + // isOnPath() in launch.js scans PATH directories directly, + // so mockDir is sufficient and no system binary directories are needed. function runDetectCli() { const testPath = mockDir; const script = [ @@ -140,6 +140,14 @@ describe("Launch Module", () => { assert.strictEqual(runDetectCli(), "claude"); }); + it("TC-CLI-072A: detectCli finds codex after claude", () => { + removeMockCmd("copilot"); + removeMockCmd("gh"); + removeMockCmd("claude"); + createMockCmd("codex"); + assert.strictEqual(runDetectCli(), "codex"); + }); + it("TC-CLI-074: gh without copilot extension is not detected as gh-copilot", () => { removeMockCmd("copilot"); removeMockCmd("claude"); @@ -249,7 +257,7 @@ describe("Launch Module", () => { .join("\n"); assert.ok( !/\bshell\s*:\s*true\b/.test(nonCommentLines), - "launch.js must not pass shell: true to spawn() — doing so splits the bootstrap prompt into multiple arguments" + "launch.js must not pass shell: true to spawn() ??? doing so splits the bootstrap prompt into multiple arguments" ); }); }); @@ -287,7 +295,7 @@ describe("Launch Module", () => { if (process.platform === "win32") { fs.writeFileSync( path.join(mockBinDir, `${binName}.cmd`), - `@"${process.execPath}" "${implScript}" %*\r\n` + `@echo off\r\n"${process.execPath}" "${implScript}" %*\r\n` ); } else { const p = path.join(mockBinDir, binName); @@ -322,8 +330,8 @@ describe("Launch Module", () => { return JSON.parse(fs.readFileSync(captureFile, "utf8")); } - for (const cliName of ["claude", "copilot", "gh-copilot"]) { - // TC-CLI-082 and TC-CLI-083 combined — run once per CLI + for (const cliName of ["claude", "copilot", "gh-copilot", "codex"]) { + // TC-CLI-082 and TC-CLI-083 combined ??? run once per CLI it(`TC-CLI-082/083: ${cliName} spawned with originalCwd and --add-dir for staging dir`, () => { const mockBinDir = path.join(cwdTestTmpDir, `mock-bin-${cliName}`); fs.mkdirSync(mockBinDir, { recursive: true }); @@ -371,7 +379,7 @@ describe("Launch Module", () => { }); describe("--dry-run flag", () => { - for (const cliName of ["copilot", "gh-copilot", "claude"]) { + for (const cliName of ["copilot", "gh-copilot", "claude", "codex"]) { it(`TC-CLI-085: --dry-run prints spawn command for ${cliName} without launching`, () => { // --dry-run must print the command and args then exit 0 without // spawning the real LLM CLI. We run with an empty PATH so that @@ -379,6 +387,13 @@ describe("Launch Module", () => { const emptyBinDir = fs.mkdtempSync( path.join(os.tmpdir(), "promptkit-dryrun-empty-") ); + if (process.platform === "win32" && cliName !== "gh-copilot") { + // Provide a local .cmd shim so resolveSpawnCommand() can find it. + fs.writeFileSync( + path.join(emptyBinDir, `${cliName}.cmd`), + "@echo off\r\nexit /b 0\r\n" + ); + } let stdout = ""; let exitCode = 0; @@ -406,10 +421,25 @@ describe("Launch Module", () => { // Parse the args line as JSON so we verify structure, not wording. const lines = stdout.split("\n"); + const cmdLine = lines.find((l) => l.trim().startsWith("cmd:")); const argsLine = lines.find((l) => l.trim().startsWith("args:")); + assert.ok(cmdLine, `--dry-run output should include a 'cmd:' line for ${cliName}`); assert.ok(argsLine, `--dry-run output should include an 'args:' line for ${cliName}`); + const parsedCmd = cmdLine.trim().slice("cmd:".length).trim(); const parsedArgs = JSON.parse(argsLine.trim().slice("args:".length).trim()); + if (cliName === "gh-copilot") { + assert.strictEqual(parsedCmd, "gh", "gh-copilot should spawn gh"); + } else if (process.platform === "win32") { + assert.strictEqual( + parsedCmd, + `${cliName}.cmd`, + `${cliName} should spawn the Windows .cmd shim` + ); + } else { + assert.strictEqual(parsedCmd, cliName, `${cliName} should spawn its bare command`); + } + // The bootstrap prompt must appear as exactly one element containing bootstrap.md, // not split across multiple elements (the shell: true regression). const bootstrapArgs = parsedArgs.filter((a) => a.includes("bootstrap.md")); @@ -436,3 +466,4 @@ describe("Launch Module", () => { } }); }); + diff --git a/docs/faq.md b/docs/faq.md index eb2bebb..837d9f0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -23,12 +23,12 @@ Yes. PromptKit generates standard Markdown prompts. The assembled output can be pasted into any LLM interface — ChatGPT, Claude, Gemini, Copilot Chat, or any other tool that accepts text input. -Interactive mode requires GitHub Copilot CLI or Claude Code, but the +Interactive mode requires GitHub Copilot CLI, Claude Code, or OpenAI Codex CLI, but the `assemble` command produces a plain text file usable anywhere. ### Do I need GitHub Copilot CLI? -No. GitHub Copilot CLI (or Claude Code) is only needed for **interactive +No. GitHub Copilot CLI (or Claude Code or OpenAI Codex CLI) is only needed for **interactive mode**, which launches a live prompt-building session. The `list` and `assemble` commands work standalone with just Node.js 18+. @@ -205,8 +205,8 @@ npx promptkit list ### Interactive mode says "No supported LLM CLI found" -Interactive mode requires GitHub Copilot CLI (`copilot`) or Claude Code -(`claude`) on your PATH. Install one of them, or use `assemble` mode +Interactive mode requires GitHub Copilot CLI (`copilot`), Claude Code +(`claude`), or OpenAI Codex CLI (`codex`) on your PATH. Install one of them, or use `assemble` mode instead. ### The assembled prompt is missing a section diff --git a/docs/getting-started.md b/docs/getting-started.md index 203b901..ac9c758 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -16,7 +16,8 @@ your behalf. - **Node.js 18+** — required for the `npx` CLI - **Optional:** [GitHub Copilot CLI](https://docs.github.com/en/copilot) - or [Claude Code](https://docs.anthropic.com/en/docs/claude-code) for + or [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or + [OpenAI Codex CLI](https://github.com/openai/codex) for interactive mode ## Quick Start @@ -59,8 +60,8 @@ and you're running. npx promptkit ``` -Interactive mode auto-detects your LLM CLI (GitHub Copilot CLI or Claude -Code), copies PromptKit's content to a temp directory, and launches an +Interactive mode auto-detects your LLM CLI (GitHub Copilot CLI, Claude +Code, or OpenAI Codex CLI), copies PromptKit's content to a temp directory, and launches an interactive session with `bootstrap.md` as the custom instruction. The bootstrap engine walks you through: