@openher/cli 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/cli.mjs +64 -55
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -73,22 +73,41 @@ function which(bin) {
73
73
  }
74
74
  }
75
75
 
76
+ // Single shared readline instance — prevents piped stdin races
77
+ let _rl = null;
78
+ function getRL() {
79
+ if (!_rl) {
80
+ _rl = createInterface({ input: process.stdin, output: process.stdout });
81
+ _rl.on("close", () => { _rl = null; });
82
+ }
83
+ return _rl;
84
+ }
85
+ function closeRL() {
86
+ if (_rl) { _rl.close(); _rl = null; }
87
+ }
88
+
76
89
  function ask(question) {
77
- const rl = createInterface({ input: process.stdin, output: process.stdout });
78
90
  return new Promise((resolve) => {
91
+ const rl = getRL();
92
+ if (!rl) { resolve(""); return; }
79
93
  rl.question(`${C.cyan}[openher]${C.reset} ${question}`, (answer) => {
80
- rl.close();
81
94
  resolve(answer.trim());
82
95
  });
83
96
  });
84
97
  }
85
98
 
99
+ // Keep event loop alive until we explicitly exit (prevents early exit with piped stdin)
100
+ const _keepAlive = setInterval(() => {}, 60000);
101
+ process.on("exit", () => { clearInterval(_keepAlive); closeRL(); });
102
+
86
103
  function askSecret(question) {
104
+ // Non-TTY fallback: just use regular readline (shows input)
105
+ if (!process.stdin.isTTY) {
106
+ return ask(question);
107
+ }
87
108
  return new Promise((resolve) => {
88
109
  process.stdout.write(`${C.cyan}[openher]${C.reset} ${question}`);
89
- const rl = createInterface({ input: process.stdin, terminal: false });
90
- // Mute echo
91
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
110
+ process.stdin.setRawMode(true);
92
111
  let buf = "";
93
112
  process.stdin.resume();
94
113
  process.stdin.on("data", function handler(chunk) {
@@ -96,14 +115,13 @@ function askSecret(question) {
96
115
  for (const ch of s) {
97
116
  if (ch === "\n" || ch === "\r") {
98
117
  process.stdin.removeListener("data", handler);
99
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
118
+ process.stdin.setRawMode(false);
100
119
  process.stdin.pause();
101
120
  process.stdout.write("\n");
102
- rl.close();
103
121
  resolve(buf);
104
122
  return;
105
123
  } else if (ch === "\u0003") {
106
- // Ctrl+C
124
+ process.stdin.setRawMode(false);
107
125
  process.exit(1);
108
126
  } else if (ch === "\x7f" || ch === "\b") {
109
127
  buf = buf.slice(0, -1);
@@ -155,6 +173,15 @@ async function install() {
155
173
  // ── Step 1: Check prerequisites ──
156
174
  log("Checking prerequisites...");
157
175
 
176
+ // Node.js version (openclaw requires v22.12+)
177
+ const nodeVer = process.versions.node.split(".").map(Number);
178
+ if (nodeVer[0] < 22 || (nodeVer[0] === 22 && nodeVer[1] < 12)) {
179
+ error(`Node.js v22.12+ required (current: v${process.versions.node})`);
180
+ console.log(" Install via: https://nodejs.org/ or nvm install 22");
181
+ process.exit(1);
182
+ }
183
+ success(`Node.js v${process.versions.node}`);
184
+
158
185
  if (!which("openclaw")) {
159
186
  error("OpenClaw not found. Please install it first:");
160
187
  console.log(" npm install -g openclaw");
@@ -356,35 +383,6 @@ async function install() {
356
383
  env: { ...process.env, PORT: String(DEFAULT_PORT) },
357
384
  });
358
385
 
359
- // Wait for startup
360
- let started = false;
361
- const startTimeout = setTimeout(() => {
362
- if (!started) {
363
- warn("Backend is still starting. Check manually:");
364
- console.log(` cd ${backendDir} && .venv/bin/python main.py`);
365
- }
366
- }, 15000);
367
-
368
- child.stdout.on("data", (data) => {
369
- const str = data.toString();
370
- if (str.includes("Uvicorn running") || str.includes("Application startup")) {
371
- started = true;
372
- clearTimeout(startTimeout);
373
- success(`Backend running on http://localhost:${DEFAULT_PORT}`);
374
- finalize();
375
- }
376
- });
377
-
378
- child.stderr.on("data", (data) => {
379
- const str = data.toString();
380
- if (str.includes("Uvicorn running") || str.includes("Application startup")) {
381
- started = true;
382
- clearTimeout(startTimeout);
383
- success(`Backend running on http://localhost:${DEFAULT_PORT}`);
384
- finalize();
385
- }
386
- });
387
-
388
386
  child.unref();
389
387
 
390
388
  // Write PID for later stop command
@@ -392,28 +390,39 @@ async function install() {
392
390
  writeFileSync(join(openherDir, "backend.pid"), String(child.pid), "utf-8");
393
391
  } catch {}
394
392
 
395
- // If backend starts quickly, finalize is called above
396
- // Otherwise, wait for the timeout
397
- if (!started) {
398
- await new Promise((resolve) => {
399
- const check = setInterval(() => {
400
- if (started) {
401
- clearInterval(check);
402
- resolve();
403
- }
404
- }, 500);
405
- // Max wait 20s
406
- setTimeout(() => {
393
+ // Wait for startup signal from stdout/stderr
394
+ let started = false;
395
+ const onOutput = (data) => {
396
+ const str = data.toString();
397
+ if (!started && (str.includes("Uvicorn running") || str.includes("Application startup"))) {
398
+ started = true;
399
+ }
400
+ };
401
+ child.stdout.on("data", onOutput);
402
+ child.stderr.on("data", onOutput);
403
+
404
+ // Poll for up to 20s
405
+ await new Promise((resolve) => {
406
+ const check = setInterval(() => {
407
+ if (started) {
407
408
  clearInterval(check);
408
409
  resolve();
409
- }, 20000);
410
- });
411
- }
410
+ }
411
+ }, 500);
412
+ setTimeout(() => {
413
+ clearInterval(check);
414
+ resolve();
415
+ }, 20000);
416
+ });
412
417
 
413
- if (!started) {
414
- warn("Backend may still be starting up...");
415
- finalize();
418
+ if (started) {
419
+ success(`Backend running on http://localhost:${DEFAULT_PORT}`);
420
+ } else {
421
+ warn("Backend may still be starting up. Check:");
422
+ console.log(` cd ${backendDir} && .venv/bin/python main.py`);
416
423
  }
424
+
425
+ finalize();
417
426
  }
418
427
 
419
428
  function finalize() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openher/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "One-click installer for OpenHer Persona Engine — AI Being plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {