From 1b0508f3cc51cdf31229e13452104d470b987420 Mon Sep 17 00:00:00 2001 From: CD Date: Thu, 9 Apr 2026 19:24:12 +1000 Subject: [PATCH] add powershell (windows) support --- IMPLEMENTATION_PLAN.md | 17 ++++++++++++++ README.md | 10 +++++++++ cli/bin/cli.js | 2 +- cli/lib/launch.js | 48 +++++++++++++++++++++++++++++++++------- cli/package.json | 1 + cli/specs/design.md | 15 ++++++++----- cli/tests/launch.test.js | 27 ++++++++++++++++++++-- docs/faq.md | 8 +++---- docs/getting-started.md | 7 +++--- 9 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 IMPLEMENTATION_PLAN.md diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..0f01b07 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,17 @@ +## Stage 1: Inspect Current CLI Launch Flow +**Goal**: Confirm how PromptKit detects and spawns supported CLIs on Windows. +**Success Criteria**: Relevant launcher code and tests identified. +**Tests**: Read existing `cli/lib/launch.js`, `cli/bin/cli.js`, and `cli/tests/launch.test.js`. +**Status**: Complete + +## Stage 2: Add Codex Windows-Compatible Launch Support +**Goal**: Support `--cli codex` and ensure Windows launches the npm `.cmd` shim instead of relying on shell-specific resolution. +**Success Criteria**: `codex` is accepted, launched correctly on Windows, and existing CLIs retain behavior. +**Tests**: Update launch unit tests for `codex` command resolution and dry-run output. +**Status**: Complete + +## Stage 3: Verify and Finalize +**Goal**: Run the CLI test suite and confirm the change is isolated. +**Success Criteria**: Relevant verification completed and docs/help text reflect Codex support. +**Tests**: `node .\\bin\\cli.js interactive --cli codex --dry-run`; attempted `node --test --test-concurrency=1 tests\\launch.test.js` +**Status**: Complete diff --git a/README.md b/README.md index 33c9b58..4d47ed6 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..7ed797f 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,13 @@ function isOnPath(cmd) { return false; } +function resolveSpawnCommand(cmd) { + if (process.platform !== "win32") return cmd; + + const shim = `${cmd}.cmd`; + return isExactFileOnPath(shim) ? shim : cmd; +} + function detectCli() { // Check for GitHub Copilot CLI first (most common) if (isOnPath("copilot")) return "copilot"; @@ -45,6 +71,7 @@ function detectCli() { } } if (isOnPath("claude")) return "claude"; + if (isOnPath("codex")) return "codex"; return null; } @@ -76,7 +103,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 +135,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 +145,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: 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/tests/launch.test.js b/cli/tests/launch.test.js index c584525..4db3885 100644 --- a/cli/tests/launch.test.js +++ b/cli/tests/launch.test.js @@ -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"); @@ -322,7 +330,7 @@ describe("Launch Module", () => { return JSON.parse(fs.readFileSync(captureFile, "utf8")); } - for (const cliName of ["claude", "copilot", "gh-copilot"]) { + 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}`); @@ -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 @@ -406,10 +414,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")); 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 fd44686..c8af471 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: