ralph-cli-sandboxed 0.1.5 → 0.2.0
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/README.md +7 -7
- package/dist/commands/docker.js +75 -54
- package/dist/commands/help.js +38 -17
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +45 -17
- package/dist/commands/once.js +4 -2
- package/dist/commands/run.js +13 -13
- package/dist/config/cli-providers.json +93 -0
- package/dist/templates/prompts.d.ts +17 -0
- package/dist/templates/prompts.js +16 -0
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +3 -2
- package/dist/utils/prompt.d.ts +2 -0
- package/dist/utils/prompt.js +136 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ ralph add
|
|
|
29
29
|
# 3. Run a single iteration
|
|
30
30
|
ralph once
|
|
31
31
|
|
|
32
|
-
# 4. Or run
|
|
33
|
-
ralph run
|
|
32
|
+
# 4. Or run until all tasks complete (default)
|
|
33
|
+
ralph run
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Commands
|
|
@@ -39,13 +39,13 @@ ralph run 5
|
|
|
39
39
|
|---------|-------------|
|
|
40
40
|
| `ralph init` | Initialize ralph in current project |
|
|
41
41
|
| `ralph once` | Run a single automation iteration |
|
|
42
|
-
| `ralph run
|
|
42
|
+
| `ralph run [n]` | Run automation iterations (default: all tasks) |
|
|
43
43
|
| `ralph add` | Add a new PRD entry (interactive) |
|
|
44
44
|
| `ralph list` | List all PRD entries |
|
|
45
45
|
| `ralph status` | Show PRD completion status |
|
|
46
46
|
| `ralph toggle <n>` | Toggle passes status for entry n |
|
|
47
47
|
| `ralph clean` | Remove all passing entries from PRD |
|
|
48
|
-
| `ralph docker
|
|
48
|
+
| `ralph docker <sub>` | Manage Docker sandbox environment |
|
|
49
49
|
| `ralph help` | Show help message |
|
|
50
50
|
|
|
51
51
|
> **Note:** `ralph prd <subcommand>` still works for compatibility (e.g., `ralph prd add`).
|
|
@@ -116,13 +116,13 @@ Run ralph in an isolated Docker container:
|
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
118
|
# Generate Docker files
|
|
119
|
-
ralph docker
|
|
119
|
+
ralph docker init
|
|
120
120
|
|
|
121
121
|
# Build the image
|
|
122
|
-
ralph docker
|
|
122
|
+
ralph docker build
|
|
123
123
|
|
|
124
124
|
# Run container
|
|
125
|
-
ralph docker
|
|
125
|
+
ralph docker run
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
Features:
|
package/dist/commands/docker.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, basename } from "path";
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { loadConfig, getRalphDir } from "../utils/config.js";
|
|
5
5
|
import { promptConfirm } from "../utils/prompt.js";
|
|
6
|
-
import { getLanguagesJson } from "../templates/prompts.js";
|
|
6
|
+
import { getLanguagesJson, getCliProvidersJson } from "../templates/prompts.js";
|
|
7
7
|
const DOCKER_DIR = "docker";
|
|
8
8
|
// Get language Docker snippet from config, with version substitution
|
|
9
9
|
function getLanguageSnippet(language, javaVersion) {
|
|
@@ -22,8 +22,20 @@ function getLanguageSnippet(language, javaVersion) {
|
|
|
22
22
|
}
|
|
23
23
|
return "\n" + snippet + "\n";
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// Get CLI provider Docker snippet from config
|
|
26
|
+
function getCliProviderSnippet(cliProvider) {
|
|
27
|
+
const cliProvidersJson = getCliProvidersJson();
|
|
28
|
+
const providerKey = cliProvider || "claude";
|
|
29
|
+
const provider = cliProvidersJson.providers[providerKey];
|
|
30
|
+
if (!provider || !provider.docker) {
|
|
31
|
+
// Default to Claude Code CLI if provider not found
|
|
32
|
+
return "# Install Claude Code CLI\nRUN curl -fsSL https://claude.ai/install.sh | bash";
|
|
33
|
+
}
|
|
34
|
+
return provider.docker.install;
|
|
35
|
+
}
|
|
36
|
+
function generateDockerfile(language, javaVersion, cliProvider) {
|
|
26
37
|
const languageSnippet = getLanguageSnippet(language, javaVersion);
|
|
38
|
+
const cliSnippet = getCliProviderSnippet(cliProvider);
|
|
27
39
|
return `# Ralph CLI Sandbox Environment
|
|
28
40
|
# Based on Claude Code devcontainer
|
|
29
41
|
# Generated by ralph-cli
|
|
@@ -32,7 +44,6 @@ FROM node:20-bookworm
|
|
|
32
44
|
|
|
33
45
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
34
46
|
ARG TZ=UTC
|
|
35
|
-
ARG CLAUDE_CODE_VERSION="latest"
|
|
36
47
|
ARG ZSH_IN_DOCKER_VERSION="1.2.1"
|
|
37
48
|
|
|
38
49
|
# Set timezone
|
|
@@ -92,11 +103,10 @@ RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/nod
|
|
|
92
103
|
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
93
104
|
echo 'fi' >> /home/node/.zshrc
|
|
94
105
|
|
|
95
|
-
|
|
96
|
-
RUN npm install -g @anthropic-ai/claude-code@\${CLAUDE_CODE_VERSION}
|
|
106
|
+
${cliSnippet}
|
|
97
107
|
|
|
98
|
-
# Install ralph-cli-
|
|
99
|
-
RUN npm install -g ralph-cli-
|
|
108
|
+
# Install ralph-cli-sandboxed from npm registry
|
|
109
|
+
RUN npm install -g ralph-cli-sandboxed
|
|
100
110
|
${languageSnippet}
|
|
101
111
|
# Setup sudo only for firewall script (no general sudo for security)
|
|
102
112
|
RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudoers.d/node-firewall
|
|
@@ -245,7 +255,7 @@ dist
|
|
|
245
255
|
.git
|
|
246
256
|
*.log
|
|
247
257
|
`;
|
|
248
|
-
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion) {
|
|
258
|
+
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider) {
|
|
249
259
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
250
260
|
// Create docker directory
|
|
251
261
|
if (!existsSync(dockerDir)) {
|
|
@@ -253,7 +263,7 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
253
263
|
console.log(`Created ${DOCKER_DIR}/`);
|
|
254
264
|
}
|
|
255
265
|
const files = [
|
|
256
|
-
{ name: "Dockerfile", content: generateDockerfile(language, javaVersion) },
|
|
266
|
+
{ name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider) },
|
|
257
267
|
{ name: "init-firewall.sh", content: FIREWALL_SCRIPT },
|
|
258
268
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName) },
|
|
259
269
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
@@ -280,9 +290,9 @@ async function buildImage(ralphDir) {
|
|
|
280
290
|
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
281
291
|
process.exit(1);
|
|
282
292
|
}
|
|
283
|
-
console.log("Building Docker image
|
|
293
|
+
console.log("Building Docker image...\n");
|
|
284
294
|
return new Promise((resolve, reject) => {
|
|
285
|
-
// Use --no-cache and --pull to ensure we always get the latest
|
|
295
|
+
// Use --no-cache and --pull to ensure we always get the latest CLI versions
|
|
286
296
|
const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
|
|
287
297
|
cwd: dockerDir,
|
|
288
298
|
stdio: "inherit",
|
|
@@ -319,7 +329,7 @@ async function imageExists(imageName) {
|
|
|
319
329
|
});
|
|
320
330
|
});
|
|
321
331
|
}
|
|
322
|
-
async function runContainer(ralphDir, imageName, language, javaVersion) {
|
|
332
|
+
async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider) {
|
|
323
333
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
324
334
|
const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
|
|
325
335
|
const hasImage = await imageExists(imageName);
|
|
@@ -327,7 +337,7 @@ async function runContainer(ralphDir, imageName, language, javaVersion) {
|
|
|
327
337
|
if (!dockerfileExists || !hasImage) {
|
|
328
338
|
if (!dockerfileExists) {
|
|
329
339
|
console.log("Docker folder not found. Initializing docker setup...\n");
|
|
330
|
-
await generateFiles(ralphDir, language, imageName, true, javaVersion);
|
|
340
|
+
await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider);
|
|
331
341
|
console.log("");
|
|
332
342
|
}
|
|
333
343
|
if (!hasImage) {
|
|
@@ -514,23 +524,24 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
514
524
|
});
|
|
515
525
|
});
|
|
516
526
|
console.log("\nDocker image and associated resources cleaned.");
|
|
517
|
-
console.log("Run 'ralph docker
|
|
527
|
+
console.log("Run 'ralph docker build' to rebuild the image.");
|
|
518
528
|
}
|
|
519
529
|
export async function docker(args) {
|
|
520
|
-
const
|
|
521
|
-
const
|
|
530
|
+
const subcommand = args[0];
|
|
531
|
+
const subArgs = args.slice(1);
|
|
522
532
|
// Show help without requiring init
|
|
523
|
-
if (
|
|
533
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
524
534
|
console.log(`
|
|
525
535
|
ralph docker - Generate and manage Docker sandbox environment
|
|
526
536
|
|
|
527
537
|
USAGE:
|
|
528
|
-
ralph docker
|
|
529
|
-
ralph docker -y
|
|
530
|
-
ralph docker
|
|
531
|
-
ralph docker
|
|
532
|
-
ralph docker
|
|
533
|
-
ralph docker
|
|
538
|
+
ralph docker init Generate Dockerfile and scripts
|
|
539
|
+
ralph docker init -y Generate files, overwrite without prompting
|
|
540
|
+
ralph docker build Build image (fetches latest CLI versions)
|
|
541
|
+
ralph docker build --clean Clean existing image and rebuild from scratch
|
|
542
|
+
ralph docker run Run container (auto-init and build if needed)
|
|
543
|
+
ralph docker clean Remove Docker image and associated resources
|
|
544
|
+
ralph docker help Show this help message
|
|
534
545
|
|
|
535
546
|
FILES GENERATED:
|
|
536
547
|
.ralph/docker/
|
|
@@ -544,11 +555,11 @@ AUTHENTICATION:
|
|
|
544
555
|
API key users: Uncomment ANTHROPIC_API_KEY in docker-compose.yml.
|
|
545
556
|
|
|
546
557
|
EXAMPLES:
|
|
547
|
-
ralph docker
|
|
548
|
-
ralph docker
|
|
549
|
-
ralph docker
|
|
550
|
-
ralph docker
|
|
551
|
-
ralph docker
|
|
558
|
+
ralph docker init # Generate files
|
|
559
|
+
ralph docker build # Build image
|
|
560
|
+
ralph docker build --clean # Clean and rebuild from scratch
|
|
561
|
+
ralph docker run # Start interactive shell
|
|
562
|
+
ralph docker clean # Remove image and volumes
|
|
552
563
|
|
|
553
564
|
# Or use docker compose directly:
|
|
554
565
|
cd .ralph/docker && docker compose run --rm ralph
|
|
@@ -578,38 +589,48 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
578
589
|
const config = loadConfig();
|
|
579
590
|
// Get image name from config or generate default
|
|
580
591
|
const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
592
|
+
const hasFlag = (flag) => subArgs.includes(flag);
|
|
593
|
+
switch (subcommand) {
|
|
594
|
+
case "build":
|
|
595
|
+
// Handle build --clean combination: clean first, then build
|
|
596
|
+
if (hasFlag("--clean")) {
|
|
597
|
+
await cleanImage(imageName, ralphDir);
|
|
598
|
+
console.log(""); // Add spacing between clean and build output
|
|
599
|
+
}
|
|
600
|
+
await buildImage(ralphDir);
|
|
601
|
+
break;
|
|
602
|
+
case "run":
|
|
603
|
+
await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider);
|
|
604
|
+
break;
|
|
605
|
+
case "clean":
|
|
606
|
+
await cleanImage(imageName, ralphDir);
|
|
607
|
+
break;
|
|
608
|
+
case "init":
|
|
609
|
+
default: {
|
|
610
|
+
// Default to init if no subcommand or unrecognized subcommand
|
|
611
|
+
const force = subcommand === "init"
|
|
612
|
+
? (subArgs[0] === "-y" || subArgs[0] === "--yes")
|
|
613
|
+
: (subcommand === "-y" || subcommand === "--yes");
|
|
614
|
+
console.log(`Generating Docker files for: ${config.language}`);
|
|
615
|
+
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
616
|
+
console.log(`Java version: ${config.javaVersion}`);
|
|
617
|
+
}
|
|
618
|
+
if (config.cliProvider && config.cliProvider !== "claude") {
|
|
619
|
+
console.log(`CLI provider: ${config.cliProvider}`);
|
|
620
|
+
}
|
|
621
|
+
console.log(`Image name: ${imageName}\n`);
|
|
622
|
+
await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider);
|
|
623
|
+
console.log(`
|
|
605
624
|
Docker files generated in .ralph/docker/
|
|
606
625
|
|
|
607
626
|
Next steps:
|
|
608
|
-
1. Build the image: ralph docker
|
|
609
|
-
2. Run container: ralph docker
|
|
627
|
+
1. Build the image: ralph docker build
|
|
628
|
+
2. Run container: ralph docker run
|
|
610
629
|
|
|
611
630
|
Or use docker compose directly:
|
|
612
631
|
cd .ralph/docker && docker compose run --rm ralph
|
|
613
632
|
`);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
614
635
|
}
|
|
615
636
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -7,23 +7,27 @@ USAGE:
|
|
|
7
7
|
COMMANDS:
|
|
8
8
|
init [opts] Initialize ralph in current project
|
|
9
9
|
once Run a single automation iteration
|
|
10
|
-
run
|
|
10
|
+
run [n] [opts] Run automation iterations (default: all tasks)
|
|
11
11
|
add Add a new PRD entry (interactive)
|
|
12
12
|
list [opts] List all PRD entries
|
|
13
13
|
status Show PRD completion status
|
|
14
14
|
toggle <n> Toggle passes status for entry n
|
|
15
15
|
clean Remove all passing entries from the PRD
|
|
16
16
|
prompt [opts] Display resolved prompt (for testing in Claude Code)
|
|
17
|
-
docker
|
|
17
|
+
docker <sub> Manage Docker sandbox environment
|
|
18
18
|
help Show this help message
|
|
19
19
|
|
|
20
20
|
prd <subcommand> (Alias) Manage PRD entries - same as add/list/status/toggle/clean
|
|
21
21
|
|
|
22
|
-
INIT
|
|
23
|
-
|
|
22
|
+
INIT:
|
|
23
|
+
The init command uses interactive prompts with arrow key navigation:
|
|
24
|
+
1. Select AI CLI provider (Claude Code, Aider, OpenCode, etc.)
|
|
25
|
+
2. Select project language/runtime
|
|
26
|
+
3. Select technology stack (if available for the language)
|
|
24
27
|
|
|
25
28
|
RUN OPTIONS:
|
|
26
|
-
|
|
29
|
+
<n> Run exactly n iterations (overrides default --all behavior)
|
|
30
|
+
--all, -a Run until all tasks are complete (default behavior)
|
|
27
31
|
--loop, -l Run continuously, waiting for new items when complete
|
|
28
32
|
--category, -c <category> Filter PRD items by category
|
|
29
33
|
Valid: ui, feature, bugfix, setup, development, testing, docs
|
|
@@ -38,13 +42,19 @@ TOGGLE OPTIONS:
|
|
|
38
42
|
<n> [n2] [n3]... Toggle one or more entries by number
|
|
39
43
|
--all, -a Toggle all PRD entries
|
|
40
44
|
|
|
45
|
+
DOCKER SUBCOMMANDS:
|
|
46
|
+
docker init Generate Dockerfile and scripts
|
|
47
|
+
docker build Build image (always fetches latest Claude Code)
|
|
48
|
+
docker run Run container (auto-init and build if needed)
|
|
49
|
+
docker clean Remove Docker image and associated resources
|
|
50
|
+
docker help Show docker help message
|
|
51
|
+
|
|
41
52
|
EXAMPLES:
|
|
42
|
-
ralph init # Initialize ralph (language selection
|
|
43
|
-
ralph init --tech-stack # Initialize with technology stack selection
|
|
53
|
+
ralph init # Initialize ralph (interactive CLI, language, tech selection)
|
|
44
54
|
ralph once # Run single iteration
|
|
45
|
-
ralph run
|
|
46
|
-
ralph run
|
|
47
|
-
ralph run
|
|
55
|
+
ralph run # Run until all tasks complete (default)
|
|
56
|
+
ralph run 5 # Run exactly 5 iterations
|
|
57
|
+
ralph run -c feature # Complete all feature tasks only
|
|
48
58
|
ralph run --loop # Run continuously until interrupted
|
|
49
59
|
ralph add # Add new PRD entry
|
|
50
60
|
ralph list # Show all entries
|
|
@@ -57,9 +67,9 @@ EXAMPLES:
|
|
|
57
67
|
ralph toggle --all # Toggle all entries
|
|
58
68
|
ralph clean # Remove passing entries
|
|
59
69
|
ralph prompt # Display resolved prompt
|
|
60
|
-
ralph docker
|
|
61
|
-
ralph docker
|
|
62
|
-
ralph docker
|
|
70
|
+
ralph docker init # Generate Dockerfile for sandboxed env
|
|
71
|
+
ralph docker build # Build Docker image
|
|
72
|
+
ralph docker run # Run container (auto-init/build if needed)
|
|
63
73
|
|
|
64
74
|
CONFIGURATION:
|
|
65
75
|
After running 'ralph init', you'll have:
|
|
@@ -70,15 +80,26 @@ CONFIGURATION:
|
|
|
70
80
|
└── progress.txt Progress tracking file
|
|
71
81
|
|
|
72
82
|
CLI CONFIGURATION:
|
|
73
|
-
The CLI tool
|
|
83
|
+
The CLI tool is configured during 'ralph init' and stored in .ralph/config.json:
|
|
74
84
|
{
|
|
75
85
|
"cli": {
|
|
76
86
|
"command": "claude",
|
|
77
|
-
"args": ["--permission-mode", "acceptEdits"]
|
|
78
|
-
|
|
87
|
+
"args": ["--permission-mode", "acceptEdits"],
|
|
88
|
+
"yoloArgs": ["--dangerously-skip-permissions"]
|
|
89
|
+
},
|
|
90
|
+
"cliProvider": "claude"
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
Available CLI providers (selected during 'ralph init'):
|
|
94
|
+
- claude: Claude Code (default)
|
|
95
|
+
- aider: AI pair programming
|
|
96
|
+
- codex: OpenAI Codex CLI
|
|
97
|
+
- gemini: Google Gemini CLI
|
|
98
|
+
- opencode: Open source AI coding agent
|
|
99
|
+
- amp: Sourcegraph AMP CLI
|
|
100
|
+
- custom: Configure your own CLI
|
|
101
|
+
|
|
102
|
+
Customize 'command', 'args', and 'yoloArgs' for other AI CLIs.
|
|
82
103
|
`;
|
|
83
104
|
export function help(_args) {
|
|
84
105
|
console.log(HELP_TEXT.trim());
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function init(
|
|
1
|
+
export declare function init(_args: string[]): Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
2
2
|
import { join, basename, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS } from "../templates/prompts.js";
|
|
5
|
-
import {
|
|
6
|
-
import { DEFAULT_CLI_CONFIG } from "../utils/config.js";
|
|
4
|
+
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders } from "../templates/prompts.js";
|
|
5
|
+
import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
|
|
7
6
|
// Get package root directory (works for both dev and installed package)
|
|
8
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
8
|
const __dirname = dirname(__filename);
|
|
@@ -14,13 +13,9 @@ const PROMPT_FILE = "prompt.md";
|
|
|
14
13
|
const PRD_FILE = "prd.json";
|
|
15
14
|
const PROGRESS_FILE = "progress.txt";
|
|
16
15
|
const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
|
|
17
|
-
function
|
|
18
|
-
return args.some(arg => flags.includes(arg));
|
|
19
|
-
}
|
|
20
|
-
export async function init(args) {
|
|
16
|
+
export async function init(_args) {
|
|
21
17
|
const cwd = process.cwd();
|
|
22
18
|
const ralphDir = join(cwd, RALPH_DIR);
|
|
23
|
-
const showTechStack = hasFlag(args, "--tech-stack", "-t");
|
|
24
19
|
console.log("Initializing ralph in current directory...\n");
|
|
25
20
|
// Check for existing .ralph directory
|
|
26
21
|
if (existsSync(ralphDir)) {
|
|
@@ -34,20 +29,51 @@ export async function init(args) {
|
|
|
34
29
|
mkdirSync(ralphDir, { recursive: true });
|
|
35
30
|
console.log(`Created ${RALPH_DIR}/`);
|
|
36
31
|
}
|
|
37
|
-
// Select
|
|
32
|
+
// Step 1: Select CLI provider (first)
|
|
33
|
+
const CLI_PROVIDERS = getCliProviders();
|
|
34
|
+
const providerKeys = Object.keys(CLI_PROVIDERS);
|
|
35
|
+
const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
36
|
+
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
37
|
+
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
38
|
+
const selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
39
|
+
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
40
|
+
let cliConfig;
|
|
41
|
+
// Handle custom CLI provider
|
|
42
|
+
if (selectedCliProviderKey === "custom") {
|
|
43
|
+
const customCommand = await promptInput("\nEnter your CLI command: ");
|
|
44
|
+
const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
|
|
45
|
+
const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
|
|
46
|
+
const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
|
|
47
|
+
const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
|
|
48
|
+
cliConfig = {
|
|
49
|
+
command: customCommand || "claude",
|
|
50
|
+
args: customArgs,
|
|
51
|
+
yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cliConfig = {
|
|
56
|
+
command: selectedProvider.command,
|
|
57
|
+
args: selectedProvider.defaultArgs,
|
|
58
|
+
yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
|
|
62
|
+
// Step 2: Select language (second)
|
|
38
63
|
const LANGUAGES = getLanguages();
|
|
39
64
|
const languageKeys = Object.keys(LANGUAGES);
|
|
40
65
|
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
41
|
-
const selectedName = await
|
|
66
|
+
const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
|
|
42
67
|
const selectedIndex = languageNames.indexOf(selectedName);
|
|
43
68
|
const selectedKey = languageKeys[selectedIndex];
|
|
44
69
|
const config = LANGUAGES[selectedKey];
|
|
45
|
-
|
|
70
|
+
console.log(`\nSelected language: ${config.name}`);
|
|
71
|
+
// Step 3: Select technology stack if available (third)
|
|
46
72
|
let selectedTechnologies = [];
|
|
47
|
-
if (
|
|
73
|
+
if (config.technologies && config.technologies.length > 0) {
|
|
48
74
|
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
49
75
|
const techNames = config.technologies.map(t => t.name);
|
|
50
|
-
selectedTechnologies = await
|
|
76
|
+
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
51
77
|
// Convert display names back to just technology names for predefined options
|
|
52
78
|
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
53
79
|
const idx = techOptions.indexOf(sel);
|
|
@@ -60,7 +86,7 @@ export async function init(args) {
|
|
|
60
86
|
console.log("\nNo technologies selected.");
|
|
61
87
|
}
|
|
62
88
|
}
|
|
63
|
-
// Allow custom commands
|
|
89
|
+
// Allow custom commands for "none" language
|
|
64
90
|
let checkCommand = config.checkCommand;
|
|
65
91
|
let testCommand = config.testCommand;
|
|
66
92
|
if (selectedKey === "none") {
|
|
@@ -81,7 +107,8 @@ export async function init(args) {
|
|
|
81
107
|
checkCommand: finalConfig.checkCommand,
|
|
82
108
|
testCommand: finalConfig.testCommand,
|
|
83
109
|
imageName,
|
|
84
|
-
cli:
|
|
110
|
+
cli: cliConfig,
|
|
111
|
+
cliProvider: selectedCliProviderKey,
|
|
85
112
|
};
|
|
86
113
|
// Add technologies if any were selected
|
|
87
114
|
if (selectedTechnologies.length > 0) {
|
|
@@ -141,6 +168,7 @@ export async function init(args) {
|
|
|
141
168
|
console.log("\nNext steps:");
|
|
142
169
|
console.log(" 1. Read .ralph/HOW-TO-WRITE-PRDs.md for guidance on writing PRDs");
|
|
143
170
|
console.log(" 2. Edit .ralph/prd.json to add your project requirements");
|
|
144
|
-
console.log(" 3. Run 'ralph
|
|
145
|
-
console.log(" 4.
|
|
171
|
+
console.log(" 3. Run 'ralph docker init' to generate Docker configuration");
|
|
172
|
+
console.log(" 4. Run 'ralph docker build' to build the container");
|
|
173
|
+
console.log(" 5. Run 'ralph docker run' to start ralph in the container");
|
|
146
174
|
}
|
package/dist/commands/once.js
CHANGED
|
@@ -15,10 +15,12 @@ export async function once(_args) {
|
|
|
15
15
|
const paths = getPaths();
|
|
16
16
|
const cliConfig = getCliConfig(config);
|
|
17
17
|
console.log("Starting single ralph iteration...\n");
|
|
18
|
-
// Build CLI arguments: config args +
|
|
18
|
+
// Build CLI arguments: config args + yolo args + prompt args
|
|
19
|
+
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
20
|
+
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
19
21
|
const cliArgs = [
|
|
20
22
|
...(cliConfig.args ?? []),
|
|
21
|
-
|
|
23
|
+
...yoloArgs,
|
|
22
24
|
"-p",
|
|
23
25
|
`@${paths.prd} @${paths.progress} ${prompt}`,
|
|
24
26
|
];
|
package/dist/commands/run.js
CHANGED
|
@@ -28,13 +28,15 @@ function createFilteredPrd(prdPath, category) {
|
|
|
28
28
|
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig) {
|
|
29
29
|
return new Promise((resolve, reject) => {
|
|
30
30
|
let output = "";
|
|
31
|
-
// Build CLI arguments: config args +
|
|
31
|
+
// Build CLI arguments: config args + yolo args + prompt args
|
|
32
32
|
const cliArgs = [
|
|
33
33
|
...(cliConfig.args ?? []),
|
|
34
34
|
];
|
|
35
|
-
// Only add
|
|
35
|
+
// Only add yolo args when running in a container
|
|
36
|
+
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
36
37
|
if (sandboxed) {
|
|
37
|
-
|
|
38
|
+
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
39
|
+
cliArgs.push(...yoloArgs);
|
|
38
40
|
}
|
|
39
41
|
// Use the filtered PRD (only incomplete items) for the prompt
|
|
40
42
|
cliArgs.push("-p", `@${filteredPrdPath} @${paths.progress} ${prompt}`);
|
|
@@ -101,7 +103,7 @@ export async function run(args) {
|
|
|
101
103
|
// Parse flags
|
|
102
104
|
let category;
|
|
103
105
|
let loopMode = false;
|
|
104
|
-
let
|
|
106
|
+
let allModeExplicit = false;
|
|
105
107
|
const filteredArgs = [];
|
|
106
108
|
for (let i = 0; i < args.length; i++) {
|
|
107
109
|
if (args[i] === "--category" || args[i] === "-c") {
|
|
@@ -119,7 +121,7 @@ export async function run(args) {
|
|
|
119
121
|
loopMode = true;
|
|
120
122
|
}
|
|
121
123
|
else if (args[i] === "--all" || args[i] === "-a") {
|
|
122
|
-
|
|
124
|
+
allModeExplicit = true;
|
|
123
125
|
}
|
|
124
126
|
else {
|
|
125
127
|
filteredArgs.push(args[i]);
|
|
@@ -131,16 +133,14 @@ export async function run(args) {
|
|
|
131
133
|
console.error(`Valid categories: ${CATEGORIES.join(", ")}`);
|
|
132
134
|
process.exit(1);
|
|
133
135
|
}
|
|
136
|
+
// Determine the mode:
|
|
137
|
+
// - If --loop is specified, use loop mode
|
|
138
|
+
// - If a specific number of iterations is provided, use that
|
|
139
|
+
// - Otherwise, default to --all mode (run until all tasks complete)
|
|
140
|
+
const hasIterationArg = filteredArgs.length > 0 && !isNaN(parseInt(filteredArgs[0])) && parseInt(filteredArgs[0]) >= 1;
|
|
141
|
+
const allMode = !loopMode && (allModeExplicit || !hasIterationArg);
|
|
134
142
|
// In loop mode or all mode, iterations argument is optional (defaults to unlimited)
|
|
135
143
|
const iterations = (loopMode || allMode) ? (parseInt(filteredArgs[0]) || Infinity) : parseInt(filteredArgs[0]);
|
|
136
|
-
if (!loopMode && !allMode && (!iterations || iterations < 1 || isNaN(iterations))) {
|
|
137
|
-
console.error("Usage: ralph run <iterations> [--category <category>]");
|
|
138
|
-
console.error(" ralph run --loop [--category <category>]");
|
|
139
|
-
console.error(" ralph run --all [--category <category>]");
|
|
140
|
-
console.error(" <iterations> must be a positive integer");
|
|
141
|
-
console.error(` <category> must be one of: ${CATEGORIES.join(", ")}`);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
144
|
requireContainer("run");
|
|
145
145
|
checkFilesExist();
|
|
146
146
|
const config = loadConfig();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"providers": {
|
|
3
|
+
"claude": {
|
|
4
|
+
"name": "Claude Code",
|
|
5
|
+
"description": "Anthropic's Claude Code CLI",
|
|
6
|
+
"command": "claude",
|
|
7
|
+
"defaultArgs": ["--permission-mode", "acceptEdits"],
|
|
8
|
+
"yoloArgs": ["--dangerously-skip-permissions"],
|
|
9
|
+
"docker": {
|
|
10
|
+
"install": "# Install Claude Code CLI\nRUN curl -fsSL https://claude.ai/install.sh | bash"
|
|
11
|
+
},
|
|
12
|
+
"envVars": ["ANTHROPIC_API_KEY"],
|
|
13
|
+
"credentialMount": "~/.claude:/home/node/.claude"
|
|
14
|
+
},
|
|
15
|
+
"aider": {
|
|
16
|
+
"name": "Aider",
|
|
17
|
+
"description": "AI pair programming in your terminal",
|
|
18
|
+
"command": "aider",
|
|
19
|
+
"defaultArgs": ["--yes"],
|
|
20
|
+
"yoloArgs": ["--yes-always"],
|
|
21
|
+
"docker": {
|
|
22
|
+
"install": "# Install Aider (requires Python)\nRUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* \\\n && pip3 install --break-system-packages --no-cache-dir aider-chat",
|
|
23
|
+
"note": "Check 'aider --help' for available flags"
|
|
24
|
+
},
|
|
25
|
+
"envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
26
|
+
"credentialMount": null
|
|
27
|
+
},
|
|
28
|
+
"codex": {
|
|
29
|
+
"name": "OpenAI Codex CLI",
|
|
30
|
+
"description": "OpenAI's Codex CLI for code generation",
|
|
31
|
+
"command": "codex",
|
|
32
|
+
"defaultArgs": ["--approval-mode", "suggest"],
|
|
33
|
+
"yoloArgs": ["--approval-mode", "full-auto"],
|
|
34
|
+
"docker": {
|
|
35
|
+
"install": "# Install OpenAI Codex CLI\nRUN npm install -g @openai/codex",
|
|
36
|
+
"note": "Check 'codex --help' for available flags"
|
|
37
|
+
},
|
|
38
|
+
"envVars": ["OPENAI_API_KEY"],
|
|
39
|
+
"credentialMount": null
|
|
40
|
+
},
|
|
41
|
+
"gemini": {
|
|
42
|
+
"name": "Gemini CLI",
|
|
43
|
+
"description": "Google's Gemini CLI for code assistance",
|
|
44
|
+
"command": "gemini",
|
|
45
|
+
"defaultArgs": [],
|
|
46
|
+
"yoloArgs": ["-y"],
|
|
47
|
+
"docker": {
|
|
48
|
+
"install": "# Install Google Gemini CLI\nRUN npm install -g @google/gemini-cli",
|
|
49
|
+
"note": "Check 'gemini --help' for available flags"
|
|
50
|
+
},
|
|
51
|
+
"envVars": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
52
|
+
"credentialMount": "~/.gemini:/home/node/.gemini"
|
|
53
|
+
},
|
|
54
|
+
"opencode": {
|
|
55
|
+
"name": "OpenCode",
|
|
56
|
+
"description": "Open source AI coding agent for the terminal",
|
|
57
|
+
"command": "opencode",
|
|
58
|
+
"defaultArgs": [],
|
|
59
|
+
"yoloArgs": ["--yolo"],
|
|
60
|
+
"docker": {
|
|
61
|
+
"install": "# Install OpenCode\nRUN curl -fsSL https://opencode.ai/install | bash \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
|
|
62
|
+
"note": "Check 'opencode --help' for available flags"
|
|
63
|
+
},
|
|
64
|
+
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY"],
|
|
65
|
+
"credentialMount": null
|
|
66
|
+
},
|
|
67
|
+
"amp": {
|
|
68
|
+
"name": "AMP CLI",
|
|
69
|
+
"description": "Sourcegraph's AMP coding agent",
|
|
70
|
+
"command": "amp",
|
|
71
|
+
"defaultArgs": [],
|
|
72
|
+
"yoloArgs": ["--yolo"],
|
|
73
|
+
"docker": {
|
|
74
|
+
"install": "# Install AMP CLI\nRUN curl -fsSL https://ampcode.com/install.sh | bash \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
|
|
75
|
+
"note": "Check 'amp --help' for available flags"
|
|
76
|
+
},
|
|
77
|
+
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
78
|
+
"credentialMount": null
|
|
79
|
+
},
|
|
80
|
+
"custom": {
|
|
81
|
+
"name": "Custom CLI",
|
|
82
|
+
"description": "Configure your own AI CLI tool",
|
|
83
|
+
"command": "",
|
|
84
|
+
"defaultArgs": [],
|
|
85
|
+
"yoloArgs": [],
|
|
86
|
+
"docker": {
|
|
87
|
+
"install": "# Custom CLI - add your installation commands here"
|
|
88
|
+
},
|
|
89
|
+
"envVars": [],
|
|
90
|
+
"credentialMount": null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -27,7 +27,24 @@ export interface LanguageConfig {
|
|
|
27
27
|
interface LanguagesJson {
|
|
28
28
|
languages: Record<string, LanguageConfigJson>;
|
|
29
29
|
}
|
|
30
|
+
export interface CliProviderConfig {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
command: string;
|
|
34
|
+
defaultArgs: string[];
|
|
35
|
+
yoloArgs: string[];
|
|
36
|
+
docker: {
|
|
37
|
+
install: string;
|
|
38
|
+
};
|
|
39
|
+
envVars: string[];
|
|
40
|
+
credentialMount: string | null;
|
|
41
|
+
}
|
|
42
|
+
interface CliProvidersJson {
|
|
43
|
+
providers: Record<string, CliProviderConfig>;
|
|
44
|
+
}
|
|
30
45
|
export declare function getLanguagesJson(): LanguagesJson;
|
|
46
|
+
export declare function getCliProvidersJson(): CliProvidersJson;
|
|
47
|
+
export declare function getCliProviders(): Record<string, CliProviderConfig>;
|
|
31
48
|
export declare function getLanguages(): Record<string, LanguageConfig>;
|
|
32
49
|
export declare const LANGUAGES: Record<string, LanguageConfig>;
|
|
33
50
|
export declare function generatePromptTemplate(): string;
|
|
@@ -11,6 +11,12 @@ function loadLanguagesConfig() {
|
|
|
11
11
|
const content = readFileSync(configPath, "utf-8");
|
|
12
12
|
return JSON.parse(content);
|
|
13
13
|
}
|
|
14
|
+
// Load CLI providers from JSON config file
|
|
15
|
+
function loadCliProvidersConfig() {
|
|
16
|
+
const configPath = join(__dirname, "..", "config", "cli-providers.json");
|
|
17
|
+
const content = readFileSync(configPath, "utf-8");
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
14
20
|
// Convert JSON config to the legacy format for compatibility
|
|
15
21
|
function convertToLanguageConfig(config) {
|
|
16
22
|
return {
|
|
@@ -24,12 +30,22 @@ function convertToLanguageConfig(config) {
|
|
|
24
30
|
// Lazy-load languages to avoid issues at import time
|
|
25
31
|
let _languagesCache = null;
|
|
26
32
|
let _languagesJsonCache = null;
|
|
33
|
+
let _cliProvidersCache = null;
|
|
27
34
|
export function getLanguagesJson() {
|
|
28
35
|
if (!_languagesJsonCache) {
|
|
29
36
|
_languagesJsonCache = loadLanguagesConfig();
|
|
30
37
|
}
|
|
31
38
|
return _languagesJsonCache;
|
|
32
39
|
}
|
|
40
|
+
export function getCliProvidersJson() {
|
|
41
|
+
if (!_cliProvidersCache) {
|
|
42
|
+
_cliProvidersCache = loadCliProvidersConfig();
|
|
43
|
+
}
|
|
44
|
+
return _cliProvidersCache;
|
|
45
|
+
}
|
|
46
|
+
export function getCliProviders() {
|
|
47
|
+
return getCliProvidersJson().providers;
|
|
48
|
+
}
|
|
33
49
|
export function getLanguages() {
|
|
34
50
|
if (!_languagesCache) {
|
|
35
51
|
const json = getLanguagesJson();
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface CliConfig {
|
|
2
2
|
command: string;
|
|
3
3
|
args?: string[];
|
|
4
|
+
yoloArgs?: string[];
|
|
4
5
|
}
|
|
5
6
|
export interface RalphConfig {
|
|
6
7
|
language: string;
|
|
@@ -11,6 +12,7 @@ export interface RalphConfig {
|
|
|
11
12
|
technologies?: string[];
|
|
12
13
|
javaVersion?: number;
|
|
13
14
|
cli?: CliConfig;
|
|
15
|
+
cliProvider?: string;
|
|
14
16
|
}
|
|
15
17
|
export declare const DEFAULT_CLI_CONFIG: CliConfig;
|
|
16
18
|
export declare function getCliConfig(config: RalphConfig): CliConfig;
|
package/dist/utils/config.js
CHANGED
|
@@ -91,8 +91,9 @@ export function requireContainer(commandName) {
|
|
|
91
91
|
console.error("For security, ralph executes AI agents only in isolated container environments.");
|
|
92
92
|
console.error("");
|
|
93
93
|
console.error("To set up a container:");
|
|
94
|
-
console.error(" ralph docker
|
|
95
|
-
console.error(" ralph docker
|
|
94
|
+
console.error(" ralph docker init # Generate Docker configuration files");
|
|
95
|
+
console.error(" ralph docker build # Build the container image");
|
|
96
|
+
console.error(" ralph docker run # Run ralph inside the container");
|
|
96
97
|
process.exit(1);
|
|
97
98
|
}
|
|
98
99
|
}
|
package/dist/utils/prompt.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export declare function createPrompt(): {
|
|
|
2
2
|
question: (query: string) => Promise<string>;
|
|
3
3
|
close: () => void;
|
|
4
4
|
};
|
|
5
|
+
export declare function promptSelectWithArrows(message: string, options: string[]): Promise<string>;
|
|
5
6
|
export declare function promptInput(message: string): Promise<string>;
|
|
6
7
|
export declare function promptSelect(message: string, options: string[]): Promise<string>;
|
|
7
8
|
export declare function promptConfirm(message: string): Promise<boolean>;
|
|
8
9
|
export declare function promptMultiSelect(message: string, options: string[]): Promise<string[]>;
|
|
10
|
+
export declare function promptMultiSelectWithArrows(message: string, options: string[]): Promise<string[]>;
|
package/dist/utils/prompt.js
CHANGED
|
@@ -13,6 +13,66 @@ export function createPrompt() {
|
|
|
13
13
|
close: () => rl.close(),
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
export async function promptSelectWithArrows(message, options) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let selectedIndex = 0;
|
|
19
|
+
// Hide cursor and enable raw mode
|
|
20
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
21
|
+
const render = () => {
|
|
22
|
+
// Move cursor up to clear previous render (except first time)
|
|
23
|
+
if (selectedIndex >= 0) {
|
|
24
|
+
process.stdout.write(`\x1B[${options.length}A`); // Move up
|
|
25
|
+
}
|
|
26
|
+
options.forEach((opt, i) => {
|
|
27
|
+
const prefix = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
28
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
29
|
+
process.stdout.write(`\x1B[2K${prefix} ${text}\n`); // Clear line and write
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const initialRender = () => {
|
|
33
|
+
console.log(`\n${message}\n`);
|
|
34
|
+
options.forEach((opt, i) => {
|
|
35
|
+
const prefix = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
36
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
37
|
+
console.log(`${prefix} ${text}`);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
initialRender();
|
|
41
|
+
// Set up raw mode for key input
|
|
42
|
+
if (process.stdin.isTTY) {
|
|
43
|
+
process.stdin.setRawMode(true);
|
|
44
|
+
}
|
|
45
|
+
process.stdin.resume();
|
|
46
|
+
process.stdin.setEncoding("utf8");
|
|
47
|
+
const onKeypress = (key) => {
|
|
48
|
+
// Handle arrow keys (escape sequences)
|
|
49
|
+
if (key === "\x1B[A" || key === "k") { // Up arrow or k
|
|
50
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
51
|
+
render();
|
|
52
|
+
}
|
|
53
|
+
else if (key === "\x1B[B" || key === "j") { // Down arrow or j
|
|
54
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
55
|
+
render();
|
|
56
|
+
}
|
|
57
|
+
else if (key === "\r" || key === "\n" || key === " ") { // Enter or space
|
|
58
|
+
cleanup();
|
|
59
|
+
resolve(options[selectedIndex]);
|
|
60
|
+
}
|
|
61
|
+
else if (key === "\x03") { // Ctrl+C
|
|
62
|
+
cleanup();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
process.stdin.removeListener("data", onKeypress);
|
|
68
|
+
if (process.stdin.isTTY) {
|
|
69
|
+
process.stdin.setRawMode(false);
|
|
70
|
+
}
|
|
71
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
72
|
+
};
|
|
73
|
+
process.stdin.on("data", onKeypress);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
16
76
|
export async function promptInput(message) {
|
|
17
77
|
const prompt = createPrompt();
|
|
18
78
|
const answer = await prompt.question(message);
|
|
@@ -99,3 +159,79 @@ export async function promptMultiSelect(message, options) {
|
|
|
99
159
|
}
|
|
100
160
|
}
|
|
101
161
|
}
|
|
162
|
+
export async function promptMultiSelectWithArrows(message, options) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
let selectedIndex = 0;
|
|
165
|
+
const selected = new Set();
|
|
166
|
+
// Add a "Done" option at the end
|
|
167
|
+
const allOptions = [...options, "[Done - press Enter]"];
|
|
168
|
+
// Hide cursor
|
|
169
|
+
process.stdout.write("\x1B[?25l");
|
|
170
|
+
const render = () => {
|
|
171
|
+
process.stdout.write(`\x1B[${allOptions.length}A`); // Move up
|
|
172
|
+
allOptions.forEach((opt, i) => {
|
|
173
|
+
const isLastOption = i === allOptions.length - 1;
|
|
174
|
+
const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
175
|
+
const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
|
|
176
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
177
|
+
process.stdout.write(`\x1B[2K${cursor} ${checkbox} ${text}\n`);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const initialRender = () => {
|
|
181
|
+
console.log(`\n${message}`);
|
|
182
|
+
console.log("(Use arrow keys to navigate, Space to select, Enter to confirm)\n");
|
|
183
|
+
allOptions.forEach((opt, i) => {
|
|
184
|
+
const isLastOption = i === allOptions.length - 1;
|
|
185
|
+
const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
186
|
+
const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
|
|
187
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
188
|
+
console.log(`${cursor} ${checkbox} ${text}`);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
initialRender();
|
|
192
|
+
if (process.stdin.isTTY) {
|
|
193
|
+
process.stdin.setRawMode(true);
|
|
194
|
+
}
|
|
195
|
+
process.stdin.resume();
|
|
196
|
+
process.stdin.setEncoding("utf8");
|
|
197
|
+
const onKeypress = (key) => {
|
|
198
|
+
if (key === "\x1B[A" || key === "k") { // Up
|
|
199
|
+
selectedIndex = (selectedIndex - 1 + allOptions.length) % allOptions.length;
|
|
200
|
+
render();
|
|
201
|
+
}
|
|
202
|
+
else if (key === "\x1B[B" || key === "j") { // Down
|
|
203
|
+
selectedIndex = (selectedIndex + 1) % allOptions.length;
|
|
204
|
+
render();
|
|
205
|
+
}
|
|
206
|
+
else if (key === " ") { // Space - toggle selection
|
|
207
|
+
const isLastOption = selectedIndex === allOptions.length - 1;
|
|
208
|
+
if (!isLastOption) {
|
|
209
|
+
if (selected.has(selectedIndex)) {
|
|
210
|
+
selected.delete(selectedIndex);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
selected.add(selectedIndex);
|
|
214
|
+
}
|
|
215
|
+
render();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (key === "\r" || key === "\n") { // Enter - confirm
|
|
219
|
+
cleanup();
|
|
220
|
+
const result = options.filter((_, i) => selected.has(i));
|
|
221
|
+
resolve(result);
|
|
222
|
+
}
|
|
223
|
+
else if (key === "\x03") { // Ctrl+C
|
|
224
|
+
cleanup();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const cleanup = () => {
|
|
229
|
+
process.stdin.removeListener("data", onKeypress);
|
|
230
|
+
if (process.stdin.isTTY) {
|
|
231
|
+
process.stdin.setRawMode(false);
|
|
232
|
+
}
|
|
233
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
234
|
+
};
|
|
235
|
+
process.stdin.on("data", onKeypress);
|
|
236
|
+
});
|
|
237
|
+
}
|