Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cli/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ program
.description("Launch an interactive session with your LLM CLI (default)")
.option(
"--cli <name>",
"LLM CLI to use (copilot, gh-copilot, claude)"
"LLM CLI to use (copilot, gh-copilot, claude, codex)"
)
.option(
"--dry-run",
Expand Down
68 changes: 59 additions & 9 deletions cli/lib/launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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";
Expand All @@ -45,6 +89,7 @@ function detectCli() {
}
}
if (isOnPath("claude")) return "claude";
if (isOnPath("codex")) return "codex";
return null;
}

Expand Down Expand Up @@ -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}`
);
Expand Down Expand Up @@ -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;
Expand All @@ -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:
Expand All @@ -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",
});
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"llm",
"ai",
"copilot",
"codex",
"prompt-templates",
"agentic-ai",
"developer-tools"
Expand Down
15 changes: 10 additions & 5 deletions cli/specs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing nit (optional fix): This line says detection uses execFileSync with where/which, but the implementation was changed to use direct PATH scanning in a previous PR. Since you're editing this section anyway, it might be worth correcting. Totally optional — not something you introduced.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, please check these should have been fixed

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 `<name>.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
Expand All @@ -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.

Expand Down Expand Up @@ -397,15 +402,15 @@ Global options:

Interactive options:
--cli <name> Override LLM CLI auto-detection
Valid values: copilot, gh-copilot, claude
Valid values: copilot, gh-copilot, claude, codex
```

### 5.2 Module Exports

**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)
}
Expand Down
Loading