ralph-cli-sandboxed 0.2.5 → 0.2.7

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.
@@ -1,7 +1,7 @@
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, getCliProviders } from "../templates/prompts.js";
4
+ import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage } from "../templates/prompts.js";
5
5
  import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
6
6
  import { dockerInit } from "./docker.js";
7
7
  // Get package root directory (works for both dev and installed package)
@@ -14,118 +14,202 @@ const PROMPT_FILE = "prompt.md";
14
14
  const PRD_FILE = "prd.json";
15
15
  const PROGRESS_FILE = "progress.txt";
16
16
  const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
17
- export async function init(_args) {
17
+ export async function init(args) {
18
18
  const cwd = process.cwd();
19
19
  const ralphDir = join(cwd, RALPH_DIR);
20
+ const useDefaults = args.includes("-y") || args.includes("--yes");
20
21
  console.log("Initializing ralph in current directory...\n");
21
22
  // Check for existing .ralph directory
22
23
  if (existsSync(ralphDir)) {
23
- const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
24
- if (!reinit) {
25
- console.log("Aborted.");
26
- return;
24
+ if (!useDefaults) {
25
+ const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
26
+ if (!reinit) {
27
+ console.log("Aborted.");
28
+ return;
29
+ }
27
30
  }
28
31
  }
29
32
  else {
30
33
  mkdirSync(ralphDir, { recursive: true });
31
34
  console.log(`Created ${RALPH_DIR}/`);
32
35
  }
33
- // Step 1: Select CLI provider (first)
34
36
  const CLI_PROVIDERS = getCliProviders();
35
- const providerKeys = Object.keys(CLI_PROVIDERS);
36
- const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
37
- const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
38
- const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
39
- const selectedCliProviderKey = providerKeys[selectedProviderIndex];
40
- const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
37
+ const LANGUAGES = getLanguages();
38
+ let selectedCliProviderKey;
41
39
  let cliConfig;
42
- // Handle custom CLI provider
43
- if (selectedCliProviderKey === "custom") {
44
- const customCommand = await promptInput("\nEnter your CLI command: ");
45
- const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
46
- const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
47
- const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
48
- const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
49
- const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
50
- const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
40
+ let selectedKey;
41
+ let selectedTechnologies = [];
42
+ let selectedSkills = [];
43
+ let checkCommand;
44
+ let testCommand;
45
+ if (useDefaults) {
46
+ // Use defaults: Claude CLI + Node.js
47
+ selectedCliProviderKey = "claude";
48
+ const provider = CLI_PROVIDERS[selectedCliProviderKey];
51
49
  cliConfig = {
52
- command: customCommand || "claude",
53
- args: customArgs,
54
- yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
55
- promptArgs: customPromptArgs,
50
+ command: provider.command,
51
+ args: provider.defaultArgs,
52
+ yoloArgs: provider.yoloArgs.length > 0 ? provider.yoloArgs : undefined,
53
+ promptArgs: provider.promptArgs ?? [],
56
54
  };
55
+ selectedKey = "node";
56
+ const config = LANGUAGES[selectedKey];
57
+ checkCommand = config.checkCommand;
58
+ testCommand = config.testCommand;
59
+ console.log(`Using defaults: ${CLI_PROVIDERS[selectedCliProviderKey].name} + ${LANGUAGES[selectedKey].name}`);
57
60
  }
58
61
  else {
59
- cliConfig = {
60
- command: selectedProvider.command,
61
- args: selectedProvider.defaultArgs,
62
- yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
63
- promptArgs: selectedProvider.promptArgs ?? [],
64
- };
65
- }
66
- console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
67
- // Step 2: Select language (second)
68
- const LANGUAGES = getLanguages();
69
- const languageKeys = Object.keys(LANGUAGES);
70
- const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
71
- const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
72
- const selectedIndex = languageNames.indexOf(selectedName);
73
- const selectedKey = languageKeys[selectedIndex];
74
- const config = LANGUAGES[selectedKey];
75
- console.log(`\nSelected language: ${config.name}`);
76
- // Step 3: Select technology stack if available (third)
77
- let selectedTechnologies = [];
78
- if (config.technologies && config.technologies.length > 0) {
79
- const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
80
- const techNames = config.technologies.map(t => t.name);
81
- selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
82
- // Convert display names back to just technology names for predefined options
83
- selectedTechnologies = selectedTechnologies.map(sel => {
84
- const idx = techOptions.indexOf(sel);
85
- return idx >= 0 ? techNames[idx] : sel;
86
- });
87
- if (selectedTechnologies.length > 0) {
88
- console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
62
+ // Step 1: Select CLI provider (first)
63
+ const providerKeys = Object.keys(CLI_PROVIDERS);
64
+ const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
65
+ const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
66
+ const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
67
+ selectedCliProviderKey = providerKeys[selectedProviderIndex];
68
+ const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
69
+ // Handle custom CLI provider
70
+ if (selectedCliProviderKey === "custom") {
71
+ const customCommand = await promptInput("\nEnter your CLI command: ");
72
+ const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
73
+ const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
74
+ const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
75
+ const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
76
+ const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
77
+ const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
78
+ cliConfig = {
79
+ command: customCommand || "claude",
80
+ args: customArgs,
81
+ yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
82
+ promptArgs: customPromptArgs,
83
+ };
89
84
  }
90
85
  else {
91
- console.log("\nNo technologies selected.");
86
+ cliConfig = {
87
+ command: selectedProvider.command,
88
+ args: selectedProvider.defaultArgs,
89
+ yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
90
+ promptArgs: selectedProvider.promptArgs ?? [],
91
+ };
92
+ }
93
+ console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
94
+ // Step 2: Select language (second)
95
+ const languageKeys = Object.keys(LANGUAGES);
96
+ const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
97
+ const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
98
+ const selectedIndex = languageNames.indexOf(selectedName);
99
+ selectedKey = languageKeys[selectedIndex];
100
+ const config = LANGUAGES[selectedKey];
101
+ console.log(`\nSelected language: ${config.name}`);
102
+ // Step 3: Select technology stack if available (third)
103
+ if (config.technologies && config.technologies.length > 0) {
104
+ const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
105
+ const techNames = config.technologies.map(t => t.name);
106
+ selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
107
+ // Convert display names back to just technology names for predefined options
108
+ selectedTechnologies = selectedTechnologies.map(sel => {
109
+ const idx = techOptions.indexOf(sel);
110
+ return idx >= 0 ? techNames[idx] : sel;
111
+ });
112
+ if (selectedTechnologies.length > 0) {
113
+ console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
114
+ }
115
+ else {
116
+ console.log("\nNo technologies selected.");
117
+ }
118
+ }
119
+ // Step 4: Select skills if available for this language
120
+ const availableSkills = getSkillsForLanguage(selectedKey);
121
+ if (availableSkills.length > 0) {
122
+ const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
123
+ const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
124
+ // Convert selected display names to SkillConfig objects
125
+ selectedSkills = selectedSkillNames.map(sel => {
126
+ const idx = skillOptions.indexOf(sel);
127
+ const skill = availableSkills[idx];
128
+ return {
129
+ name: skill.name,
130
+ description: skill.description,
131
+ instructions: skill.instructions,
132
+ userInvocable: skill.userInvocable,
133
+ };
134
+ });
135
+ if (selectedSkills.length > 0) {
136
+ console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
137
+ }
138
+ else {
139
+ console.log("\nNo skills selected.");
140
+ }
141
+ }
142
+ // Allow custom commands for "none" language
143
+ checkCommand = config.checkCommand;
144
+ testCommand = config.testCommand;
145
+ if (selectedKey === "none") {
146
+ checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
147
+ testCommand = await promptInput("Enter your test command: ") || testCommand;
92
148
  }
93
- }
94
- // Allow custom commands for "none" language
95
- let checkCommand = config.checkCommand;
96
- let testCommand = config.testCommand;
97
- if (selectedKey === "none") {
98
- checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
99
- testCommand = await promptInput("Enter your test command: ") || testCommand;
100
149
  }
101
150
  const finalConfig = {
102
- ...config,
151
+ ...LANGUAGES[selectedKey],
103
152
  checkCommand,
104
153
  testCommand,
105
154
  };
106
155
  // Generate image name from directory name
107
156
  const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
108
157
  const imageName = `ralph-${projectName}`;
109
- // Write config file
158
+ // Write config file with all available options (defaults or empty values)
110
159
  const configData = {
160
+ // Required fields
111
161
  language: selectedKey,
112
162
  checkCommand: finalConfig.checkCommand,
113
163
  testCommand: finalConfig.testCommand,
114
164
  imageName,
165
+ // CLI configuration
115
166
  cli: cliConfig,
116
167
  cliProvider: selectedCliProviderKey,
168
+ // Optional fields with defaults/empty values for discoverability
169
+ notifyCommand: "",
170
+ technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
171
+ javaVersion: selectedKey === "java" ? 21 : null,
172
+ // Docker configuration options
173
+ docker: {
174
+ ports: [],
175
+ volumes: [],
176
+ environment: {},
177
+ git: {
178
+ name: "",
179
+ email: "",
180
+ },
181
+ packages: [],
182
+ buildCommands: {
183
+ root: [],
184
+ node: [],
185
+ },
186
+ startCommand: "",
187
+ asciinema: {
188
+ enabled: false,
189
+ autoRecord: false,
190
+ outputDir: ".recordings",
191
+ streamJson: {
192
+ enabled: false,
193
+ saveRawJson: true,
194
+ },
195
+ },
196
+ firewall: {
197
+ allowedDomains: [],
198
+ },
199
+ },
200
+ // Claude-specific configuration (MCP servers and skills)
201
+ claude: {
202
+ mcpServers: {},
203
+ skills: selectedSkills,
204
+ },
117
205
  };
118
- // Add technologies if any were selected
119
- if (selectedTechnologies.length > 0) {
120
- configData.technologies = selectedTechnologies;
121
- }
122
206
  const configPath = join(ralphDir, CONFIG_FILE);
123
207
  writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
124
208
  console.log(`\nCreated ${RALPH_DIR}/${CONFIG_FILE}`);
125
209
  // Write prompt file (ask if exists) - uses template with $variables
126
210
  const prompt = generatePromptTemplate();
127
211
  const promptPath = join(ralphDir, PROMPT_FILE);
128
- if (existsSync(promptPath)) {
212
+ if (existsSync(promptPath) && !useDefaults) {
129
213
  const overwritePrompt = await promptConfirm(`${RALPH_DIR}/${PROMPT_FILE} already exists. Overwrite?`);
130
214
  if (overwritePrompt) {
131
215
  writeFileSync(promptPath, prompt + "\n");
@@ -137,7 +221,7 @@ export async function init(_args) {
137
221
  }
138
222
  else {
139
223
  writeFileSync(promptPath, prompt + "\n");
140
- console.log(`Created ${RALPH_DIR}/${PROMPT_FILE}`);
224
+ console.log(`${existsSync(promptPath) ? "Updated" : "Created"} ${RALPH_DIR}/${PROMPT_FILE}`);
141
225
  }
142
226
  // Create PRD if not exists
143
227
  const prdPath = join(ralphDir, PRD_FILE);
@@ -1,6 +1,139 @@
1
1
  import { spawn } from "child_process";
2
+ import { existsSync, appendFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
2
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
3
5
  import { resolvePromptVariables } from "../templates/prompts.js";
6
+ /**
7
+ * Parses a stream-json line and extracts displayable text.
8
+ * Formats output similar to Claude Code's normal terminal display.
9
+ */
10
+ function parseStreamJsonLine(line, debug = false) {
11
+ try {
12
+ const json = JSON.parse(line);
13
+ if (debug && json.type) {
14
+ process.stderr.write(`[stream-json] type: ${json.type}\n`);
15
+ }
16
+ // Handle Claude Code CLI stream-json events
17
+ const type = json.type;
18
+ switch (type) {
19
+ // === Text Content ===
20
+ case "content_block_delta":
21
+ // Incremental text updates - the main streaming content
22
+ if (json.delta?.type === "text_delta") {
23
+ return json.delta.text || "";
24
+ }
25
+ // Tool input being streamed
26
+ if (json.delta?.type === "input_json_delta") {
27
+ return ""; // Don't show partial JSON, wait for complete tool call
28
+ }
29
+ return json.delta?.text || "";
30
+ case "text":
31
+ return json.text || "";
32
+ // === Tool Use ===
33
+ case "content_block_start":
34
+ if (json.content_block?.type === "tool_use") {
35
+ const toolName = json.content_block?.name || "unknown";
36
+ return `\n── Tool: ${toolName} ──\n`;
37
+ }
38
+ if (json.content_block?.type === "text") {
39
+ return json.content_block?.text || "";
40
+ }
41
+ return "";
42
+ case "content_block_stop":
43
+ // End of a content block - add newline after tool use
44
+ return "";
45
+ // === Tool Results ===
46
+ case "tool_result":
47
+ const toolOutput = json.content || json.output || "";
48
+ const truncated = typeof toolOutput === "string" && toolOutput.length > 500
49
+ ? toolOutput.substring(0, 500) + "... (truncated)"
50
+ : toolOutput;
51
+ return `\n── Tool Result ──\n${typeof truncated === "string" ? truncated : JSON.stringify(truncated, null, 2)}\n`;
52
+ // === Assistant Messages ===
53
+ case "assistant":
54
+ const contents = json.message?.content || json.content || [];
55
+ let output = "";
56
+ for (const block of contents) {
57
+ if (block.type === "text") {
58
+ output += block.text || "";
59
+ }
60
+ else if (block.type === "tool_use") {
61
+ output += `\n── Tool: ${block.name} ──\n`;
62
+ if (block.input) {
63
+ output += JSON.stringify(block.input, null, 2) + "\n";
64
+ }
65
+ }
66
+ }
67
+ return output;
68
+ case "message_start":
69
+ // Beginning of a new message
70
+ return "\n";
71
+ case "message_delta":
72
+ // Message completion info (stop_reason, usage)
73
+ if (json.delta?.stop_reason) {
74
+ return `\n[${json.delta.stop_reason}]\n`;
75
+ }
76
+ return "";
77
+ case "message_stop":
78
+ return "\n";
79
+ // === System/User Events ===
80
+ case "system":
81
+ if (json.message) {
82
+ return `[System] ${json.message}\n`;
83
+ }
84
+ return "";
85
+ case "user":
86
+ // User message echo - usually not needed to display
87
+ return "";
88
+ // === Results and Errors ===
89
+ case "result":
90
+ if (json.result !== undefined) {
91
+ return `\n── Result ──\n${JSON.stringify(json.result, null, 2)}\n`;
92
+ }
93
+ return "";
94
+ case "error":
95
+ const errMsg = json.error?.message || JSON.stringify(json.error);
96
+ return `\n[Error] ${errMsg}\n`;
97
+ // === File Operations (Claude Code specific) ===
98
+ case "file_edit":
99
+ case "file_write":
100
+ const filePath = json.path || json.file || "unknown";
101
+ return `\n── Writing: ${filePath} ──\n`;
102
+ case "file_read":
103
+ const readPath = json.path || json.file || "unknown";
104
+ return `── Reading: ${readPath} ──\n`;
105
+ case "bash":
106
+ case "command":
107
+ const cmd = json.command || json.content || "";
108
+ return `\n── Running: ${cmd} ──\n`;
109
+ case "bash_output":
110
+ case "command_output":
111
+ const cmdOutput = json.output || json.content || "";
112
+ return cmdOutput + "\n";
113
+ default:
114
+ // Fallback: check for common text fields
115
+ if (json.text)
116
+ return json.text;
117
+ if (json.content && typeof json.content === "string")
118
+ return json.content;
119
+ if (json.message && typeof json.message === "string")
120
+ return json.message;
121
+ if (json.output && typeof json.output === "string")
122
+ return json.output;
123
+ if (debug) {
124
+ process.stderr.write(`[stream-json] unhandled type: ${type}, keys: ${Object.keys(json).join(", ")}\n`);
125
+ }
126
+ return "";
127
+ }
128
+ }
129
+ catch (e) {
130
+ // Not valid JSON
131
+ if (debug) {
132
+ process.stderr.write(`[stream-json] parse error: ${e}\n`);
133
+ }
134
+ return "";
135
+ }
136
+ }
4
137
  export async function once(args) {
5
138
  // Parse flags
6
139
  let debug = false;
@@ -32,7 +165,19 @@ export async function once(args) {
32
165
  });
33
166
  const paths = getPaths();
34
167
  const cliConfig = getCliConfig(config);
35
- console.log("Starting single ralph iteration...\n");
168
+ // Check if stream-json output is enabled
169
+ const streamJsonConfig = config.docker?.asciinema?.streamJson;
170
+ const streamJsonEnabled = streamJsonConfig?.enabled ?? false;
171
+ const saveRawJson = streamJsonConfig?.saveRawJson !== false; // default true
172
+ const outputDir = config.docker?.asciinema?.outputDir || ".recordings";
173
+ console.log("Starting single ralph iteration...");
174
+ if (streamJsonEnabled) {
175
+ console.log("Stream JSON output enabled - displaying formatted Claude output");
176
+ if (saveRawJson) {
177
+ console.log(`Raw JSON logs will be saved to: ${outputDir}/`);
178
+ }
179
+ }
180
+ console.log();
36
181
  // Build CLI arguments: config args + yolo args + model args + prompt args
37
182
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
38
183
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
@@ -42,6 +187,20 @@ export async function once(args) {
42
187
  ...(cliConfig.args ?? []),
43
188
  ...yoloArgs,
44
189
  ];
190
+ // Add stream-json output format if enabled
191
+ let jsonLogPath;
192
+ if (streamJsonEnabled) {
193
+ cliArgs.push("--output-format", "stream-json", "--verbose", "--print");
194
+ // Setup JSON log file if saving raw JSON
195
+ if (saveRawJson) {
196
+ const fullOutputDir = join(process.cwd(), outputDir);
197
+ if (!existsSync(fullOutputDir)) {
198
+ mkdirSync(fullOutputDir, { recursive: true });
199
+ }
200
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
201
+ jsonLogPath = join(fullOutputDir, `ralph-once-${timestamp}.jsonl`);
202
+ }
203
+ }
45
204
  // Add model args if model is specified
46
205
  if (model && cliConfig.modelArgs) {
47
206
  cliArgs.push(...cliConfig.modelArgs, model);
@@ -49,19 +208,98 @@ export async function once(args) {
49
208
  cliArgs.push(...promptArgs, promptValue);
50
209
  if (debug) {
51
210
  console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
211
+ if (jsonLogPath) {
212
+ console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
213
+ }
52
214
  }
53
215
  return new Promise((resolve, reject) => {
54
- const proc = spawn(cliConfig.command, cliArgs, {
55
- stdio: "inherit",
56
- });
57
- proc.on("close", (code) => {
58
- if (code !== 0) {
59
- console.error(`\n${cliConfig.command} exited with code ${code}`);
60
- }
61
- resolve();
62
- });
63
- proc.on("error", (err) => {
64
- reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
65
- });
216
+ if (streamJsonEnabled) {
217
+ // Stream JSON mode: capture stdout, parse JSON, display clean text
218
+ let lineBuffer = "";
219
+ const proc = spawn(cliConfig.command, cliArgs, {
220
+ stdio: ["inherit", "pipe", "inherit"],
221
+ });
222
+ proc.stdout.on("data", (data) => {
223
+ const chunk = data.toString();
224
+ lineBuffer += chunk;
225
+ const lines = lineBuffer.split("\n");
226
+ // Keep the last incomplete line in the buffer
227
+ lineBuffer = lines.pop() || "";
228
+ for (const line of lines) {
229
+ const trimmedLine = line.trim();
230
+ if (!trimmedLine)
231
+ continue;
232
+ // Check if this is a JSON line
233
+ if (trimmedLine.startsWith("{")) {
234
+ // Save raw JSON if enabled
235
+ if (jsonLogPath) {
236
+ try {
237
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
238
+ }
239
+ catch {
240
+ // Ignore write errors
241
+ }
242
+ }
243
+ // Parse and display clean text
244
+ const text = parseStreamJsonLine(trimmedLine, debug);
245
+ if (text) {
246
+ process.stdout.write(text);
247
+ }
248
+ }
249
+ else {
250
+ // Non-JSON line - display as-is (might be status messages, errors, etc.)
251
+ process.stdout.write(trimmedLine + "\n");
252
+ }
253
+ }
254
+ });
255
+ proc.on("close", (code) => {
256
+ // Process any remaining buffered content
257
+ if (lineBuffer.trim()) {
258
+ const trimmedLine = lineBuffer.trim();
259
+ if (trimmedLine.startsWith("{")) {
260
+ if (jsonLogPath) {
261
+ try {
262
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
263
+ }
264
+ catch {
265
+ // Ignore write errors
266
+ }
267
+ }
268
+ const text = parseStreamJsonLine(trimmedLine, debug);
269
+ if (text) {
270
+ process.stdout.write(text);
271
+ }
272
+ }
273
+ else {
274
+ // Non-JSON remaining content
275
+ process.stdout.write(trimmedLine + "\n");
276
+ }
277
+ }
278
+ // Ensure final newline
279
+ process.stdout.write("\n");
280
+ if (code !== 0) {
281
+ console.error(`\n${cliConfig.command} exited with code ${code}`);
282
+ }
283
+ resolve();
284
+ });
285
+ proc.on("error", (err) => {
286
+ reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
287
+ });
288
+ }
289
+ else {
290
+ // Standard mode: pass through all I/O
291
+ const proc = spawn(cliConfig.command, cliArgs, {
292
+ stdio: "inherit",
293
+ });
294
+ proc.on("close", (code) => {
295
+ if (code !== 0) {
296
+ console.error(`\n${cliConfig.command} exited with code ${code}`);
297
+ }
298
+ resolve();
299
+ });
300
+ proc.on("error", (err) => {
301
+ reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
302
+ });
303
+ }
66
304
  });
67
305
  }