@lawrence369/loop-cli 0.1.1 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.2] - 2026-03-10
9
+
10
+ ### Fixed
11
+
12
+ - Fix `posix_spawnp failed` crash: `@clack/prompts` placeholder text was leaking as actual CLI arguments
13
+ - Add try-catch around PTY spawn with clear error message when engine CLI is not found
14
+ - Add automatic fallback to non-interactive `engine.run()` when PTY spawn fails
15
+
8
16
  ## [0.1.0] - 2026-03-10
9
17
 
10
18
  ### Added
@@ -76,13 +76,20 @@ export class PtySession extends EventEmitter {
76
76
  const rows = opts?.rows ?? process.stdout.rows ?? 24;
77
77
  this._engine = opts?.engine;
78
78
  this._promptPattern = DEFAULT_PROMPT_PATTERN;
79
- this._pty = pty.spawn(command, args, {
80
- name: "xterm-256color",
81
- cols,
82
- rows,
83
- cwd,
84
- env: { ...process.env, ...(opts?.env ?? {}) },
85
- });
79
+ try {
80
+ this._pty = pty.spawn(command, args, {
81
+ name: "xterm-256color",
82
+ cols,
83
+ rows,
84
+ cwd,
85
+ env: { ...process.env, ...(opts?.env ?? {}) },
86
+ });
87
+ }
88
+ catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ throw new Error(`Failed to spawn PTY for "${command}": ${msg}\n` +
91
+ `Ensure "${command}" is installed and available in your PATH.`);
92
+ }
86
93
  // ── PTY data handler ──────────────────────────────────────────────
87
94
  this._pty.onData((data) => {
88
95
  if (!this._alive)
@@ -109,14 +109,38 @@ async function runPtySession(engine, initialPrompt, opts) {
109
109
  // Set up renderer before spawning the PTY so the first frame is not missed
110
110
  const renderer = new PtyRenderer(engine.name, engine.label);
111
111
  renderer.start();
112
- // Create PTY session via engine.interactive()
113
- const session = engine.interactive({
114
- cwd: opts.cwd,
115
- passthroughArgs: opts.passthroughArgs,
116
- onData(data) {
117
- renderer.write(data);
118
- },
119
- });
112
+ // Create PTY session via engine.interactive(), with fallback to engine.run()
113
+ let session;
114
+ try {
115
+ session = engine.interactive({
116
+ cwd: opts.cwd,
117
+ passthroughArgs: opts.passthroughArgs,
118
+ onData(data) {
119
+ renderer.write(data);
120
+ },
121
+ });
122
+ }
123
+ catch (err) {
124
+ // PTY spawn failed — fall back to non-interactive engine.run()
125
+ const msg = err instanceof Error ? err.message : String(err);
126
+ renderer.stop({ elapsed: "0.0s", bytes: "0 B" });
127
+ console.log(dim(` PTY spawn failed: ${msg}`));
128
+ console.log(dim(` Falling back to non-interactive mode...\n`));
129
+ const start = Date.now();
130
+ const output = await engine.run(initialPrompt, {
131
+ cwd: opts.cwd,
132
+ verbose: opts.verbose,
133
+ passthroughArgs: opts.passthroughArgs,
134
+ onData(chunk) {
135
+ process.stdout.write(chunk);
136
+ },
137
+ });
138
+ return {
139
+ output,
140
+ bytes: Buffer.byteLength(output),
141
+ durationMs: Date.now() - start,
142
+ };
143
+ }
120
144
  return new Promise((resolve, reject) => {
121
145
  let done = false;
122
146
  let idleTimer = null;
@@ -93,17 +93,20 @@ export async function interactive() {
93
93
  return null;
94
94
  }
95
95
  // Native CLI flags (optional)
96
+ const PASS_ARGS_PLACEHOLDER = "e.g., --model claude-sonnet-4-20250514";
96
97
  const passArgsInput = await p.text({
97
98
  message: "Native CLI flags for executor (optional)",
98
- placeholder: "e.g., --model claude-sonnet-4-20250514",
99
+ placeholder: PASS_ARGS_PLACEHOLDER,
99
100
  defaultValue: "",
100
101
  });
101
102
  if (p.isCancel(passArgsInput)) {
102
103
  p.cancel("Cancelled.");
103
104
  return null;
104
105
  }
105
- const passthroughArgs = passArgsInput.trim()
106
- ? passArgsInput.split(/\s+/).filter(Boolean)
106
+ // Guard against @clack/prompts returning placeholder text as the value
107
+ const rawPassArgs = typeof passArgsInput === "string" ? passArgsInput : "";
108
+ const passthroughArgs = rawPassArgs.trim() && rawPassArgs.trim() !== PASS_ARGS_PLACEHOLDER
109
+ ? rawPassArgs.split(/\s+/).filter(Boolean)
107
110
  : [];
108
111
  // Task
109
112
  const task = await p.text({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawrence369/loop-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Iterative Multi-Engine AI Orchestration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",